1

I am creating a mobile app (Xamarin + MvvmCross) which accesses my services Web API 2 + Owin and am trying to support external login. For the following, assume I have already registered the user with external auth and just want to log the user in from the mobile app.

Currently I have Facebook login working primarily following this ASP.NET Web API 2 external logins with Facebook and Google in AngularJS app, however after many hours, I can't figure out how to implement the same strategy for Microsoft logins as I can't find a way to verify the Microsoft access token.

My understanding is that in order to call my own APIs (e.g. get my user data and objects) I need to exchange the external access token for a local access token from my own service by first doing the following:

  1. Authenticate with external provider (in mobile app)
  2. Send the providers access token to my Server (Web Api 2)
  3. Validate the access token with the provider to determine if the user is legit - in the process receiving the ProviderKey (userId in Facebook and google)
  4. Log in the user locally using the Provider Name and UserId from step 3
  5. Generate a local access token for the currently logged in user.

Facebook currently works through that whole process listed above on the web and mobile app. With Microsoft, I can authenticate on the mobile app and get the Microsoft access token however, I am stuck at step 3, validating the access token on my server to receive any useful information I can use to identify the already registered user in my app.

In order to identify the correct user, I need the Login Provider and ProviderKey from the AspNetUserLogins table.

After receiving the Microsoft Access Token, how can I validate the token and check for matching provider names and provider keys?

Is this the common/standard approach for external auth? I thought there would have been a lot more information about it if that was the case. All and any help is greatly appreciated.

Extra Info

I am also trying to support both normal Microsoft Accounts as well as Work/School accounts using the new Microsoft Graph Api (unified office365). As such, it appears there are different provider keys depending on Microsoft/Organisation account e.g. https://login.microsoftonline.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/v2.0

Code Examples

Step 3 - Verifying the external access token to get user login information

Private async Task<ParsedExternalAccessToken> VerifyExternalAccessToken(string provider, string accessToken) {
            ParsedExternalAccessToken parsedToken = null;

            var verifyTokenEndPoint = "";

            if (provider == Resources.Constants.FacebookProvider) {
                //You can get it from here: https://developers.facebook.com/tools/accesstoken/
                //More about debug_tokn here: https://stackoverflow.com/questions/16641083/how-does-one-get-the-app-access-token-for-debug-token-inspection-on-facebook

                var appToken = WebConfigurationManager.AppSettings["fb_app_token"];
                verifyTokenEndPoint = string.Format("https://graph.facebook.com/debug_token?input_token={0}&access_token={1}", accessToken, appToken);
            } else if (provider == Resources.Constants.GoogleProvider) {
                verifyTokenEndPoint = string.Format("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={0}", accessToken);
            } else if (provider == "Microsoft" || provider == Resources.Constants.MicrosoftAccountProvider || provider == Resources.Constants.MicrooftSchoolOrWorkAccountProvider) {
                //made up end point -> what is the real answer/solution?
                verifyTokenEndPoint = string.Format("https://login.microsoftonline.com/common/v2.0/tokeninfo?access_token={0}", accessToken);

            } else {
                return null;
            }

            var client = new HttpClient();
            var uri = new Uri(verifyTokenEndPoint);
            var response = await client.GetAsync(uri);

            if (response.IsSuccessStatusCode) {
                var content = await response.Content.ReadAsStringAsync();

                dynamic jObj = (JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(content);

                parsedToken = new ParsedExternalAccessToken();

                if (provider == Resources.Constants.FacebookProvider) {
                    parsedToken.user_id = jObj["data"]["user_id"];
                    parsedToken.app_id = jObj["data"]["app_id"];

                    if (!string.Equals(Startup.facebookAuthOptions.AppId, parsedToken.app_id, StringComparison.OrdinalIgnoreCase)) {
                        return null;
                    }
                } else if (provider == Resources.Constants.GoogleProvider) {
                    parsedToken.user_id = jObj["user_id"];
                    parsedToken.app_id = jObj["audience"];

                    if (!string.Equals(Startup.googleAuthOptions.ClientId, parsedToken.app_id, StringComparison.OrdinalIgnoreCase)) {
                        return null;
                    }

                } else if (provider == Resources.Constants.MicrosoftAccountProvider || provider == Resources.Constants.MicrooftSchoolOrWorkAccountProvider) {
                    throw new NotImplementedException("Microsoft Access Token Validation not implemented");
                }

            }

            return parsedToken;
        }

Step 4 - Obtain Local Access Token

public async Task<IHttpActionResult> ObtainLocalAccessToken(string provider, string externalAccessToken) {

            if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(externalAccessToken)) {
                return ApiErrorResult(ServiceResults.ExternalAuth.Codes.ProviderOrExternalAccessTokenIsNotSent, ServiceResults.ExternalAuth.Messages.ProviderOrExternalAccessTokenIsNotSent);
            }

            var verifiedAccessToken = await VerifyExternalAccessToken(provider, externalAccessToken);
            if (verifiedAccessToken == null) {
                return ApiErrorResult(ServiceResults.ExternalAuth.Codes.InvalidProviderOrExternalAccessToken, ServiceResults.ExternalAuth.Messages.InvalidProviderOrExternalAccessToken);
            }

            var user = await _userManager.FindAsync(new UserLoginInfo(provider, verifiedAccessToken.user_id));

            bool hasRegistered = user != null;

            if (!hasRegistered) {
                return ApiErrorResult(ServiceResults.ExternalAuth.Codes.ExternalUserIsNotRegistered, ServiceResults.ExternalAuth.Messages.ExternalUserIsNotRegistered);
            }

            //generate access token response
            var accessTokenResponse = await GenerateLocalOauthToken(user);

            return Ok(accessTokenResponse);

        }

Thank you

UPDATE

I've tried implementing both of @dstrockis options with limited success. On both options, I still can't figure out what to use to match to the ProviderKey

When making the graph Api call to GET User, no values match the ProviderKey stored, such as the userId used in Facebook and Google. The Microsoft ProviderKey value looks like: AAAAAAAAAAAAAAAAAAAAADWmHuzvvAQpO*******9PM.

I also managed to verify the access token using the JWT Validation libraries as suggested. Looking through the claims, I can identify the correct LoginProvider now (good start) but I still can't find any values which match what is stored in the ProviderKey.

This value must have something to do with the user but doesn't match anything directly. Does anyone know where the ProviderKey comes from with OpenIdAuthentication for Microsoft. Is it encrypted from another value?

Validating JWT token

private async Task ValidateMicrosoftToken(string token) {
            string stsDiscoveryEndpoint = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
            ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint);
            OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync();

            TokenValidationParameters validationParameters = new TokenValidationParameters {
                ValidateAudience = false,
                ValidateIssuer = false,
                IssuerSigningTokens = config.SigningTokens,
                ValidateLifetime = false
            };

            JwtSecurityTokenHandler tokendHandler = new JwtSecurityTokenHandler();

            SecurityToken jwt;

            var result = tokendHandler.ValidateToken(token, validationParameters, out jwt);

            //result contains claims with lots of values
            //get provider key from claims????
        }

Auth Provider - Startup.Auth

microsoftAccountAuthOptions = new OpenIdConnectAuthenticationOptions() {
                Description = new AuthenticationDescription() { AuthenticationType = "OpenIdConnect", Caption = "Microsoft"},
                AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
                ClientId = appId,
                Authority = authority,
                Scope = "openid user.read email " + string.Join(" ", scopes),
                RedirectUri = redirectUri,
                //PostLogoutRedirectUri = "/",
                TokenValidationParameters = new TokenValidationParameters {
                    //.....
                },
                Notifications = new OpenIdConnectAuthenticationNotifications {
                    //.....
                }
            };
            app.UseOpenIdConnectAuthentication(microsoftAccountAuthOptions);
Community
  • 1
  • 1
Robert
  • 827
  • 9
  • 16

1 Answers1

0

This approach is perfectly fine. For step 3, you really have two options:

Best of luck!

dstrockis
  • 1,173
  • 5
  • 6