Persist additional claims and tokens from external providers in ASP.NET Core

[ad_1]










By Luke Latham

An ASP.NET Core app can establish additional claims and tokens from external authentication providers, such as Facebook, Google, Microsoft, and Twitter. Each provider reveals different information about users on its platform, but the pattern for receiving and transforming user data into additional claims is the same.

View or download sample code (how to download)

Prerequisites

Decide which external authentication providers to support in the app. For each provider, register the app and obtain a client ID and client secret. For more information, see Facebook, Google, and external provider authentication in ASP.NET Core. The sample app uses the Google authentication provider.

Set the client ID and client secret

The OAuth authentication provider establishes a trust relationship with an app using a client ID and client secret. Client ID and client secret values are created for the app by the external authentication provider when the app is registered with the provider. Each external provider that the app uses must be configured independently with the provider’s client ID and client secret. For more information, see the external authentication provider topics that apply to your scenario:

The sample app configures the Google authentication provider with a client ID and client secret provided by Google:

services.AddAuthentication().AddGoogle(options =>

    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
    // Provide the Google Secret
    options.ClientSecret = "g4GZ2#...GD5Gg1x";
    options.Scope.Add("https://www.googleapis.com/auth/plus.login");
    options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
    options.SaveTokens = true;
    options.Events.OnCreatingTicket = ctx =>
    
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens() 
            as List<AuthenticationToken>;
        tokens.Add(new AuthenticationToken()
        
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        );
        ctx.Properties.StoreTokens(tokens);
        return Task.CompletedTask;
    ;
);

Establish the authentication scope

Specify the list of permissions to retrieve from the provider by specifying the Scope. Authentication scopes for common external providers appear in the following table.

Provider Scope
Facebook https://www.facebook.com/dialog/oauth
Google https://www.googleapis.com/auth/plus.login
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

The sample app adds the Google plus.login scope to request Google+ sign in permissions:

services.AddAuthentication().AddGoogle(options =>

    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
    // Provide the Google Secret
    options.ClientSecret = "g4GZ2#...GD5Gg1x";
    options.Scope.Add("https://www.googleapis.com/auth/plus.login");
    options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
    options.SaveTokens = true;
    options.Events.OnCreatingTicket = ctx =>
    
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens() 
            as List<AuthenticationToken>;
        tokens.Add(new AuthenticationToken()
        
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        );
        ctx.Properties.StoreTokens(tokens);
        return Task.CompletedTask;
    ;
);

Map user data keys and create claims

In the provider’s options, specify a MapJsonKey for each key in the external provider’s JSON user data for the app identity to read on sign in. For more information on claim types, see ClaimTypes.

The sample app creates a Gender claim from the gender key in Google user data:

services.AddAuthentication().AddGoogle(options =>

    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
    // Provide the Google Secret
    options.ClientSecret = "g4GZ2#...GD5Gg1x";
    options.Scope.Add("https://www.googleapis.com/auth/plus.login");
    options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
    options.SaveTokens = true;
    options.Events.OnCreatingTicket = ctx =>
    
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens() 
            as List<AuthenticationToken>;
        tokens.Add(new AuthenticationToken()
        
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        );
        ctx.Properties.StoreTokens(tokens);
        return Task.CompletedTask;
    ;
);

In OnPostConfirmationAsync, an IdentityUser (ApplicationUser) is signed into the app with SignInAsync. During the sign in process, the UserManager<TUser> can store an ApplicationUser claim for user data available from the Principal.

In the sample app, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) establishes a Gender claim for the signed in ApplicationUser:

public async Task<IActionResult> OnPostConfirmationAsync(
    string returnUrl = null)
{
    if (ModelState.IsValid)
    
        // Get the information about the user from the external login
        // provider
        var info = await _signInManager.GetExternalLoginInfoAsync();

        if (info == null)
        
            throw new ApplicationException(
                "Error loading external login data during confirmation.");
        

        var user = new ApplicationUser 
            
                UserName = Input.Email, 
                Email = Input.Email
            ;
        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            
                // Copy over the gender claim
                await _userManager.AddClaimAsync(user, 
                    info.Principal.FindFirst(ClaimTypes.Gender));

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);

                await _signInManager.SignInAsync(user, props, 
                    authenticationMethod: info.LoginProvider);
                _logger.LogInformation(
                    "User created an account using Name provider.", 
                    info.LoginProvider);

                return LocalRedirect(Url.GetLocalUrl(returnUrl));
            
        

        foreach (var error in result.Errors)
        
            ModelState.AddModelError(string.Empty, error.Description);
        
    

    ReturnUrl = returnUrl;

    return Page();
}

Save the access token

SaveTokens defines whether access and refresh tokens should be stored in the AuthenticationProperties after a successful authorization. SaveTokens is set to false by default to reduce the size of the final authentication cookie.

The sample app sets the value of SaveTokens to true in GoogleOptions:

services.AddAuthentication().AddGoogle(options =>

    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
    // Provide the Google Secret
    options.ClientSecret = "g4GZ2#...GD5Gg1x";
    options.Scope.Add("https://www.googleapis.com/auth/plus.login");
    options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
    options.SaveTokens = true;
    options.Events.OnCreatingTicket = ctx =>
    
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens() 
            as List<AuthenticationToken>;
        tokens.Add(new AuthenticationToken()
        
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        );
        ctx.Properties.StoreTokens(tokens);
        return Task.CompletedTask;
    ;
);

When OnPostConfirmationAsync executes, store the access token (ExternalLoginInfo.AuthenticationTokens) from the external provider in the ApplicationUser‘s AuthenticationProperties.

The sample app saves the access token in:

  • OnPostConfirmationAsync – Executes for new user registration.
  • OnGetCallbackAsync – Executes when a previously registered user signs into the app.

Account/ExternalLogin.cshtml.cs:

public async Task<IActionResult> OnPostConfirmationAsync(
    string returnUrl = null)
{
    if (ModelState.IsValid)
    
        // Get the information about the user from the external login
        // provider
        var info = await _signInManager.GetExternalLoginInfoAsync();

        if (info == null)
        
            throw new ApplicationException(
                "Error loading external login data during confirmation.");
        

        var user = new ApplicationUser 
            
                UserName = Input.Email, 
                Email = Input.Email
            ;
        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            
                // Copy over the gender claim
                await _userManager.AddClaimAsync(user, 
                    info.Principal.FindFirst(ClaimTypes.Gender));

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);

                await _signInManager.SignInAsync(user, props, 
                    authenticationMethod: info.LoginProvider);
                _logger.LogInformation(
                    "User created an account using Name provider.", 
                    info.LoginProvider);

                return LocalRedirect(Url.GetLocalUrl(returnUrl));
            
        

        foreach (var error in result.Errors)
        
            ModelState.AddModelError(string.Empty, error.Description);
        
    

    ReturnUrl = returnUrl;

    return Page();
}
public async Task<IActionResult> OnGetCallbackAsync(
    string returnUrl = null, string remoteError = null)

    if (remoteError != null)
    
        ErrorMessage = $"Error from external provider: remoteError";

        return RedirectToPage("./Login");
    

    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    
        return RedirectToPage("./Login");
    

    // Sign in the user with this external login provider if the user 
    // already has a login
    var result = await _signInManager.ExternalLoginSignInAsync(
        info.LoginProvider, info.ProviderKey, isPersistent: false, 
        bypassTwoFactor : true);

    if (result.Succeeded)
    
        // Store the access token and resign in so the token is included in
        // in the cookie
        var user = await _userManager.FindByLoginAsync(info.LoginProvider, 
            info.ProviderKey);

        var props = new AuthenticationProperties();
        props.StoreTokens(info.AuthenticationTokens);

        await _signInManager.SignInAsync(user, props, info.LoginProvider);

        _logger.LogInformation(
            "Name logged in with LoginProvider provider.", 
            info.Principal.Identity.Name, info.LoginProvider);

        return LocalRedirect(Url.GetLocalUrl(returnUrl));
    

    if (result.IsLockedOut)
    
        return RedirectToPage("./Lockout");
    
    else
    
        // If the user does not have an account, then ask the user to 
        // create an account
        ReturnUrl = returnUrl;
        LoginProvider = info.LoginProvider;

        if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
        
            Input = new InputModel
            
                Email = info.Principal.FindFirstValue(ClaimTypes.Email)
            ;
        

        return Page();
    

How to add additional custom tokens

To demonstrate how to add a custom token, which is stored as part of SaveTokens, the sample app adds an AuthenticationToken with the current DateTime for an AuthenticationToken.Name of TicketCreated:

services.AddAuthentication().AddGoogle(options =>

    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
    // Provide the Google Secret
    options.ClientSecret = "g4GZ2#...GD5Gg1x";
    options.Scope.Add("https://www.googleapis.com/auth/plus.login");
    options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
    options.SaveTokens = true;
    options.Events.OnCreatingTicket = ctx =>
    
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens() 
            as List<AuthenticationToken>;
        tokens.Add(new AuthenticationToken()
        
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        );
        ctx.Properties.StoreTokens(tokens);
        return Task.CompletedTask;
    ;
);

Sample app instructions

The sample app demonstrates how to:

  • Obtain the user’s gender from Google and store a gender claim with the value.
  • Store the Google access token in the user’s AuthenticationProperties.

To use the sample app:

  1. Register the app and obtain a valid client ID and client secret for Google authentication. For more information, see Google external login setup in ASP.NET Core.
  2. Provide the client ID and client secret to the app in the GoogleOptions of Startup.ConfigureServices.
  3. Run the app and request the My Claims page. When the user isn’t signed in, the app redirects to Google. Sign in with Google. Google redirects the user back to the app (/Home/MyClaims). The user is authenticated, and the My Claims page is loaded. The gender claim is present under User Claims with the value obtained from Google. The access token appears in the Authentication Properties.
User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    b36a7b09-9135-4810-b7a5-78697ff23e99
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
    username@gmail.com
AspNet.Identity.SecurityStamp
    29G2TB881ATCUQFJSRFG1S0QJ0OOAWVT
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/gender
    female
http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod
    Google

Authentication Properties

.Token.access_token
    bv42.Dgw...GQMv9ArLPs
.Token.token_type
    Bearer
.Token.expires_at
    2018-08-27T19:08:00.0000000+00:00
.Token.TicketCreated
    8/27/2018 6:08:00 PM
.TokenNames
    access_token;token_type;expires_at;TicketCreated
.issued
    Mon, 27 Aug 2018 18:08:05 GMT
.expires
    Mon, 10 Sep 2018 18:08:05 GMT

Forward request information with a proxy or load balancer

If the app is deployed behind a proxy server or load balancer, some of the original request information might be forwarded to the app in request headers. This information usually includes the secure request scheme (https), host, and client IP address. Apps don’t automatically read these request headers to discover and use the original request information.

The scheme is used in link generation that affects the authentication flow with external providers. Losing the secure scheme (https) results in the app generating incorrect insecure redirect URLs.

Use Forwarded Headers Middleware to make the original request information available to the app for request processing.

For more information, see Configure ASP.NET Core to work with proxy servers and load balancers.



[ad_2]

source_link
https://www.asp.net

Leave a Reply

8 + دوازده =