Search code examples
oauth-2.0asp.net-core-2.0razor-pagesasp.net-core-2.1asp.net-core-identity

ASP.NET Core 2.1: how to access "action claims" in the controller code (especially during user registration)?


Using Core Identity OAuth2 authentication (and retrieving user info) last MS samples uses such code:

   options.Events = new OAuthEvents
   {   
       // ...
       OnCreatingTicket = async (OAuthCreatingTicketContext context) =>
       {
          var userInfo = // ..
          context.RunClaimActions(userInfo);
       }
   }

MS call it "Action Claims".

But how to access those action claims later, in the controller code ? To be concrete, how to access them in the "Core Identity 2.1" generated Razor pages user external login OnGetCallbackAsync

// ExternalLogin.cshtml.cs
 public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
 {
         var claimsIdentity = User.Identity as ClaimsIdentity; 
         var claimsPrincipal = User.Identity as ClaimsPrincipal; // null
         // claimsIdentity doesn't contains oauth target claims (since this new not registered yet user?)
         // ..
         var signInResult = await _signInManager.ExternalLoginSignInAsync(...);
        if (signInResult.Succeeded)
        {
        }else // means yet not registered locally 
        {
            // HOW TO ACCESS ACTION CLAIMS THERE?
            // or how to get authentication token to get user info manually...
        }
  }

P.S.
Additionally to the asnwer: RunClaimActions should be used together with MapJsonKey

serviceCollection.AddAuthentication().AddOAuth(options =>
            { 
              // ...
                    // https://msdn.microsoft.com/en-us/library/microsoft.identitymodel.claims.claimtypes_members.aspx

                    options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
                    options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
                    options.ClaimActions.MapJsonKey("SalesforceOrganizationId", "organization_id");

Then userinfo fields can be accessed as regular user claims. Therefore "Action claims" are not "special type of claims" but just "yet another ASP MVC magic".

Also do not forget about options.SaveTokens = true; Only with it you will be able to get token as

var info = await _signInManager.GetExternalLoginInfoAsync();
var token = info.AuthenticationTokens ()[0];

and get more information from other connected services.


Solution

  • I've seen this example in official documentation.

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

    You would first have to map the desired claims when configuring the authentication provider

    The documentation example used Google's, where it 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.

    Startup

    services.AddAuthentication().AddGoogle(options => {
    
        //....
    
        options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
        //...map other claims/claim types
    
        //...
    }
    

    And from there you should be able to access claims via the ExternalLoginInfo.Principal which has the ClaimsPrincipal representing the user associated with the login.

    ExternalLogin.cshtml.cs

    //Executes when a previously registered user signs into the app.
    public async Task<IActionResult> OnGetCallbackAsync(
        string returnUrl = null, string remoteError = null) {
        if (remoteError != null) {
            ErrorMessage = $"Error from external provider: {remoteError}";
            return RedirectToPage("./Login");
        }
    
        // Get the information about the user from the external login provider
        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);
    
            // What is the gender of this user if present
            if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Gender)) {
                var gender = info.Principal.FindFirst(ClaimTypes.Gender);
                //...use gender
            }
    
            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();
        }
    }
    

    Review the comments in the code and note the access to info.Principal.Identity, which should contain the claims associated with the current user after resigning in.