Redplane
Redplane

Reputation: 3151

Manually generate IdentityServer4 reference token and save to PersistedGrants table

I have learnt IdentityServer4 for a week and successfully implemented a simple authentication flow with ResourceOwnedPassword flow.

Now, I'm implementing Google authentication with IdentityServer4 by following this tutorial

This is what I am doing:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    //...

    const string connectionString = @"Data Source=.\SQLEXPRESS;database=IdentityServer4.Quickstart.EntityFramework-2.0.0;trusted_connection=yes;";
    var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

    services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddProfileService<IdentityServerProfileService>()
        .AddResourceOwnerValidator<IdentityResourceOwnerPasswordValidator>()
        // this adds the config data from DB (clients, resources)
        .AddConfigurationStore(options =>
        {
            options.ConfigureDbContext = builder =>
            {
                builder.UseSqlServer(connectionString,
                    sql => sql.MigrationsAssembly(migrationsAssembly));
            };

        })
        // this adds the operational data from DB (codes, tokens, consents)
        .AddOperationalStore(options =>
        {
            options.ConfigureDbContext = builder =>
                builder.UseSqlServer(connectionString,
                    sql => sql.MigrationsAssembly(migrationsAssembly));
            // this enables automatic token cleanup. this is optional.
            options.EnableTokenCleanup = true;
            options.TokenCleanupInterval = 30;
        });

    // Add jwt validation.
    services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddIdentityServerAuthentication(options =>
        {
            // base-address of your identityserver
            options.Authority = "https://localhost:44386";

            options.ClaimsIssuer = "https://localhost:44386";

            // name of the API resource
            options.ApiName = "api1";
            options.ApiSecret = "secret";

            options.RequireHttpsMetadata = false;

            options.SupportedTokens = SupportedTokens.Reference;

        });

    //...
}

** Google controller (Which is for handling returned token from Google **

public class GLoginController : Controller
    {
        #region Properties

        private readonly IPersistedGrantStore _persistedGrantStore;

        private readonly IUserFactory _userFactory;

        private readonly IBaseTimeService _baseTimeService;

        private readonly ITokenCreationService _tokenCreationService;

        private readonly IReferenceTokenStore _referenceTokenStore;

        private readonly IBaseEncryptionService _baseEncryptionService;

        #endregion

        #region Constructor

        public GLoginController(IPersistedGrantStore persistedGrantStore,
            IBaseTimeService basetimeService,
            ITokenCreationService tokenCreationService,
            IReferenceTokenStore referenceTokenStore,
            IBaseEncryptionService baseEncryptionService,
            IUserFactory userFactory)
        {
            _persistedGrantStore = persistedGrantStore;
            _baseTimeService = basetimeService;
            _userFactory = userFactory;
            _tokenCreationService = tokenCreationService;
            _referenceTokenStore = referenceTokenStore;
            _baseEncryptionService = baseEncryptionService;
        }

        #endregion

        #region Methods

        [HttpGet("login")]
        [AllowAnonymous]
        public IActionResult Login()
        {
            var authenticationProperties = new AuthenticationProperties
            {
                RedirectUri = "/api/google/handle-external-login"
            };

            return Challenge(authenticationProperties, "Google");
        }

        [HttpGet("handle-external-login")]
        //[Authorize("ExternalCookie")]
        [AllowAnonymous]
        public async Task<IActionResult> HandleExternalLogin()
        {
            //Here we can retrieve the claims
            var authenticationResult = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
            var principal = authenticationResult.Principal;

            var emailAddress = principal.FindFirst(ClaimTypes.Email)?.Value;
            if (string.IsNullOrEmpty(emailAddress))
                return NotFound(new ApiMessageViewModel("Email is not found"));

            // Find user by using username.
            var loadUserConditions = new LoadUserModel();
            loadUserConditions.Usernames = new HashSet<string> { emailAddress };
            loadUserConditions.Pagination = new PaginationValueObject(1, 1);

            // Find users asynchronously.
            var loadUsersResult = await _userFactory.FindUsersAsync(loadUserConditions);
            var user = loadUsersResult.FirstOrDefault();

            // User is not defined.
            if (user == null)
            {
                user = new User(Guid.NewGuid(), emailAddress);
                user.Email = emailAddress;
                user.HashedPassword = _baseEncryptionService.Md5Hash("abcde12345-");
                user.JoinedTime = _baseTimeService.DateTimeUtcToUnix(DateTime.UtcNow);
                user.Kind = UserKinds.Google;
                user.Status = UserStatuses.Active;

                //await _userFactory.AddUserAsync(user);
            }
            else
            {
                // User is not google account.
                if (user.Kind != UserKinds.Google)
                    return Forbid("User is not allowed to access system.");
            }

            var token = new Token(IdentityServerConstants.TokenTypes.IdentityToken);
            var userCredential = new UserCredential(user);

            token.Claims = userCredential.GetClaims();
            token.AccessTokenType = AccessTokenType.Reference;
            token.ClientId = "ro.client";
            token.CreationTime = DateTime.UtcNow;
            token.Audiences = new[] {"api1"};
            token.Lifetime = 3600;


            return Ok();
        }

        #endregion
    }

Everything is fine, I can get claims returned from Google OAuth2, find users in database using Google email address and register them if they dont have any acounts.

My question is: How can I use Google OAuth2 claims that I receive in HandleExternalLogin method to generate a Reference Token, save it to PersistedGrants table and return to client.

This means when user access https://localhost:44386/api/google/login, after being redirected to Google consent screen, they can receive access_token, refresh_token that has been generated by IdentityServer4.

Thank you,

Upvotes: 3

Views: 4367

Answers (1)

d_f
d_f

Reputation: 4859

  • In IdentityServer the kind (jwt of reference) of the token is configurable for each client (application), requested the token.
  • AccessTokenType.Reference is valid for TokenTypes.AccessToken not TokenTypes.IdentityToken as in your snippet.

In general it would be simpler to follow the original quickstart and then extend the generic code up to your needs. What I can see now in the snippet above is just your specific stuff and not the default part, responsible for creating the IdSrv session and redirecting back to the client.

If you still like to create a token manually:

  • inject ITokenService into your controller.
  • fix the error I mentioned above: TokenTypes.AccessToken instead of TokenTypes.IdentityToken
  • call var tokenHandle = await TokenService.CreateAccessTokenAsync(token);

tokenHandle is a key in PersistedGrantStore

Upvotes: 1

Related Questions