Search code examples
authenticationasp.net-coreidentityserver4access-tokenopenid-connect

Flow external login tokens from the Identity Server to the client app


I have setup IdentityServer4 based on .net core 2.2 and configured Xero as an External Login using OpenIdConnect middleware. I have a client app which configures the IdentityServer for Authentication. What I like to access in the client app is not only authentication tokens from the IdentityServer but also the tokens from External login. There is a MS documentation which suggests to include the external login tokens in OnGetCallbackAsync :

var props = new AuthenticationProperties();
props.StoreTokens(info.AuthenticationTokens);
props.IsPersistent = true;
await _signInManager.SignInAsync(user, props);

Since my IdentityServer template doesn't have OnGetCallbackAsync method, I assumed implementing above in ExternalLoginCallback action from ExternalLoginController will do the job (I may be wrong):

public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
    {
        var context = await _interactionService.GetAuthorizationContextAsync(returnUrl);

        if (remoteError != null)
        {
            ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}");

            return View($"~/Login/{nameof(LoginController.Login)}");
        }

        var info = await _signInManager.GetExternalLoginInfoAsync();

        if (info == null)
        {
            return RedirectToAction(nameof(LoginController.Login), "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,
            Constants.AuthenticationProps.Defaults.IsPersistent);

        var emailClaim = ClaimTypes.Email;

        if (result.Succeeded)
        {                
            var user = await _userManager.FindByNameAsync(info.Principal.FindFirstValue(emailClaim));

            var props = new AuthenticationProperties();
            props.StoreTokens(info.AuthenticationTokens);
            props.IsPersistent = true;
            await _signInManager.SignInAsync(user, props, info.LoginProvider);

            await _events.RaiseAsync(new UserLoginSuccessEvent(user.Email, user.Id.ToString(), $"{user.GivenName} {user.FamilyName}"));
            await _events.RaiseAsync(new UserLoginEvent(user.Id, context?.ClientId));
            _logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider);
            return RedirectToLocal(returnUrl);
        } ...

So the login is working and I get Identity Server tokens by HttpContext.GetTokenAsync("access_token") in the client app(asp.net core) however still can't figure out how to access external login tokens in the client app. I'm not sure if I'm missing something or whether this is the correct approach to flow the external login tokens to my client app and if so how to access those AuthenticationTokens in the client app?

here is my client app openidconnect configuration for reference :

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = "oidc";
        })
        .AddCookie(
            CookieAuthenticationDefaults.AuthenticationScheme,
            options =>
        {
            options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
            options.Cookie.Name = "xero";
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        })
        .AddOpenIdConnect("oidc", options =>
        {
            options.Authority = "https://localhost:44333";//IdentityServer address
            options.RequireHttpsMetadata = true;
            options.ClientId = "MyAppClientId";
            options.ClientSecret = "MyAppClientSecret";
            options.ResponseType = "code id_token";
            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;
            options.Scope.Clear();
            options.Scope.Add("openid");
            options.Scope.Add("profile");
            options.Scope.Add("email");
            options.ClaimActions.MapAllExcept("iss", "nbf", "exp", "aud", "nonce", "iat", "c_hash");

            options.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = JwtClaimTypes.Name,
                RoleClaimType = JwtClaimTypes.Role
            };

            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true
            };

            options.Events = new OpenIdConnectEvents
            {
                OnRedirectToIdentityProvider = ctx =>
                {
                    if (ctx.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Authentication)
                    {
                        ctx.ProtocolMessage.AcrValues = "idp:xero";
                    }
                    return Task.CompletedTask;
                }
            };
        });

Solution

  • I got it working by adding a missing part in ExternalLoginCallback. I replaced the following codes:

    var props = new AuthenticationProperties();
    props.StoreTokens(info.AuthenticationTokens);
    props.IsPersistent = true;
    await _signInManager.SignInAsync(user, props, info.LoginProvider);
    

    with this line:

    await _signInManager.UpdateExternalAuthenticationTokensAsync(info);

    Which saves the external login credentials(access_token , expires_at, id_token, refresh_token and token_type) in Identity SQL DB in [dbo].[UserTokens]. All I need next is to fetch the data from the Table in some way.

    Including those credentials in Identity token is not a great idea and can make the response header too big and client request may fail so I implemented an API to be called by my client app by sending user id and receiving external tokens. Thanks to @NanYu for the hint.