Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
                }
            };
        });
like image 910
Amir Chatrbahr Avatar asked Oct 21 '25 12:10

Amir Chatrbahr


2 Answers

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.

like image 106
Amir Chatrbahr Avatar answered Oct 23 '25 07:10

Amir Chatrbahr


If implementing external login in Identity Server , after Identity server receive id token/access token from external provider , it will decode the token and get user's claims , sign in user , then create identity server's own tokens and at last return to your client app . Identity Server will not handle the tokens form external provider , but you can get the tokens in Callback method of ExternalController :

var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);

//get the tokens
var tokens = result.Properties.GetTokens();
var idToken = tokens.Where(x => x.Name.Equals("id_token")).FirstOrDefault().Value;

Then you can preserve whatever data you want , cache tokens , and return to client side ,for example , in token response like this sample . Of course your client app could also make another request to get the tokens after authentication(preserve tokens in ExternalController).

like image 31
Nan Yu Avatar answered Oct 23 '25 07:10

Nan Yu



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!