Search code examples
oauth-2.0asp.net-web-api2cross-platformgoogle-oauth

Crossplatform multiclient oauth2 API design


I am building RESTfull backend with ASP.NET Web API 2. FTM I have three clients: AngularJs web application, Android, and IPhone app.

I need to build login via Google and Facebook for all of them and need your help to design it.

My idea is the following (using Google auth as an example):

  1. in Google Developer Console I create 4 "OAuth 2.0 client IDs":

    • for AngularJS app (type Web application, with allowed origins)
    • for Android (type Android)
    • for IOs (type IOs)
    • for WEB API backend (type Web application without any allowed origins)
  2. every client will contact Google with its own libraries/apis to authorize and request User consent on the scopes needed but also for offline use. So that authorization_code is received by the client from Google.

  3. every client will call web backend GET method ExternalLogin and send authorization_code as an input parameter.

  4. web backend will exchange (via Google API) re authorization_code to the access_token and receive all needed User information from Google (via Google API). Check the database if User already registered (if not - app will create an account) and then issue local access token that should be used for all the further calls to web backend.

The benefits are pretty straightforward:

  • simple flow, the same for every client
  • a single method to login/register for every client
  • the logic to collect User data is in one place (web backend) - so if We need extra data to collect from the User we need to make a change in the backend only. Clients need to add scopes to consent screen only.

My questions are:

  • is the following flow correct and stable enough? Will it also work for Facebook auth?
  • FTM I am stuck at step 4. AngularJs got authorization_code with its ClientId. Web backend is trying to exchange this authorization_code for access_token with another ClientId (web backend has its own ClientId in Google developer console, read above) I am getting an error: unauthorized_client. Strange, cause all ClientIDs in the same "Project" in google developer console. Is there a way to make it work?

Solution

  • The flow is pretty much ok, with one exception: you need to setup client IDs only for the actual client applications (hence the Authorized JavaScript origins), and not for the backend, too.

    Facebook is pretty much the same on the backend side at least (just a different library for validation), you should have no problems abstracting away a generic provider interface.

    As for validating the integrity of the ID token on the server side - do a GET on Google's endpoint to verify this token, let themworry about the validation algorithm: https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={0}

    Here's how this could look:

    private const string GoogleApiTokenInfoUrl = "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={0}";
    
    public ProviderUserDetails GetUserDetails(string providerToken)
    {
        var httpClient = new MonitoredHttpClient();
        var requestUri = new Uri(string.Format(GoogleApiTokenInfoUrl, providerToken));
    
        HttpResponseMessage httpResponseMessage;
        try
        {
            httpResponseMessage = httpClient.GetAsync(requestUri).Result;
        }
        catch (Exception ex)
        {
            return null;
        }
    
        if (httpResponseMessage.StatusCode != HttpStatusCode.OK)
        {
            return null;
        }
    
        var response = httpResponseMessage.Content.ReadAsStringAsync().Result;
        var googleApiTokenInfo = JsonConvert.DeserializeObject<GoogleApiTokenInfo>(response);
    
        if (!SupportedClientsIds.Contains(googleApiTokenInfo.aud))
        {
            Log.WarnFormat("Google API Token Info aud field ({0}) not containing the required client id", googleApiTokenInfo.aud);
            return null;
        }
    
        return new ProviderUserDetails
        {
            Email = googleApiTokenInfo.email,
            FirstName = googleApiTokenInfo.given_name,
            LastName = googleApiTokenInfo.family_name,
            Locale = googleApiTokenInfo.locale,
            Name = googleApiTokenInfo.name,
            ProviderUserId = googleApiTokenInfo.sub
        };
    }