question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Refresh token randomly expires in rc1 & 2 after migrating from v2

See original GitHub issue

Describe the bug

Our project was working perfectly in v2 and after migrating to v3 we have found that our refresh tokens are invalid, the code returns the following exception

image

This just happens randomly when we refresh our page, have noticed it as we are developing with angular and when changes to client side code are saved, the page refreshes. Usually this is fine as our refresh token would be renewed, but it seems to log us out.

Here you can see that the refresh token method is called with a grant_type or refresh_token image

Setup of openid is very simple

// the default value for AllowuserNameCharacters is "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"
// here we have just added some additional characters
services.AddIdentity<User, Role>(options => { options.User.AllowedUserNameCharacters += "'&"; })
	.AddDefaultTokenProviders();

// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
	options.ClaimsIdentity.UserNameClaimType = Claims.Name;
	options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
	options.ClaimsIdentity.RoleClaimType = Claims.Role;
});

// return unauthorized message instead of url
services.ConfigureApplicationCookie(options =>
{
	options.Events.OnRedirectToLogin = context =>
	{
		context.Response.StatusCode = 401;
		return System.Threading.Tasks.Task.CompletedTask;
	};
});

// configure all tokens generated from aspnet to expire in 3 days (create password, forget password etc)
services.Configure<DataProtectionTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromDays(3));

// authentication
var authenticationBuilder = services.AddAuthentication(options =>
{
	options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
	options.DefaultForbidScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});

// OpenIddict
services.AddOpenIddict()
	.AddCore(options =>
	{
		options.UseEntityFramework(e => e.UseDbContext<ApplicationDbContext>());
	})
	.AddServer(options =>
	{
		// For token lifetimes look at the below link
		// https://github.com/openiddict/openiddict-core/wiki/Configuration-and-options

		// Enable the authorization, logout, token endpoints.
		options.SetAuthorizationEndpointUris("/connect/authorize")
			   .SetLogoutEndpointUris("/connect/logout")
			   .SetTokenEndpointUris("/connect/token");

		// Note: the Mvc.Client sample only uses the code flow and the password flow, but you
		// can enable the other flows if you need to support implicit or client credentials.
		options.AllowPasswordFlow()
			   .AllowRefreshTokenFlow();

		// Mark the "email", "profile" scopes as supported scopes.
		options.RegisterScopes(Scopes.Email,
							   Scopes.OpenId,
							   Scopes.Profile,
							   Scopes.OfflineAccess);

		// code to allow requests without client_id
		options.AcceptAnonymousClients();

		// Register a new ephemeral key, that is discarded when the application
		// shuts down. Tokens signed using this key are automatically invalidated.
		// This method should only be used during development.
		options.AddEncryptionCertificate(new System.Security.Cryptography.X509Certificates.X509Certificate2(Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "signcert.pfx"), "MYPASSWORDGOESHERE"));
		options.AddSigningCertificate(new System.Security.Cryptography.X509Certificates.X509Certificate2(Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "signcert.pfx"), "MYPASSWORDGOESHERE"));

		// Register the ASP.NET Core MVC binder used by OpenIddict.
		// Note: if you don't call this method, you won't be able to
		// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
		options.UseAspNetCore()
			   .EnableAuthorizationEndpointPassthrough()
			   .EnableLogoutEndpointPassthrough()
			   .EnableTokenEndpointPassthrough()
			   .DisableTransportSecurityRequirement();  // Never use https because we use load balancer
	})
	.AddValidation(options =>
	{
		// Import the configuration from the local OpenIddict server instance.
		options.UseLocalServer();

		// set the audience
		options.AddAudiences("http://localhost:5000/");

		// Register the ASP.NET Core host.
		options.UseAspNetCore();
	});

I have my authorizationController.cs file as well, which has been updated to v3. When the token is expired, it never gets to the request.IsRefreshTokenGrantType() check

/// <summary>
/// Handles authorization requests
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class AuthorizationController : Controller
{
	private OpenIddictApplicationManager<OpenIddictEntityFrameworkApplication> _applicationManager;
	private SignInManager<MyUser> _signInManager;
	private UserManager<MyUser> _userManager;
	private AppSettings _appSettings;
	private IEncryptionService _encryptionService;
	private IActivIdService _activIdService;
	private IUserLogService _userLogService;

	/// <summary>
	/// Create a new authorization controller
	/// </summary>
	/// <param name="applicationManager"></param>
	/// <param name="signInManager"></param>
	/// <param name="userManager"></param>
	/// <param name="appSettings"></param>
	/// <param name="encryptionService"></param>
	public AuthorizationController(
		IOptions<IdentityOptions> identityOptions,
		OpenIddictApplicationManager<OpenIddictEntityFrameworkApplication> applicationManager,
		SignInManager<MyUser> signInManager, UserManager<MyUser> userManager,
		AppSettings appSettings, IEncryptionService encryptionService)
	{
		_applicationManager = applicationManager;
		_signInManager = signInManager;
		_userManager = userManager;
		_appSettings = appSettings;
		_encryptionService = encryptionService;
	}

	// Note: to support interactive flows like the code flow,
	// you must provide your own authorization endpoint action:

	/// <summary>
	/// Authorize an openId request
	/// </summary>
	/// <param name="connectRequest"></param>
	/// <returns></returns>
	[Authorize, HttpGet, Route("~/connect/authorize")]
	public async Task<IActionResult> Authorize()
	{
		var request = HttpContext.GetOpenIddictServerRequest() ??
			throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

		// Retrieve the application details from the database.
		var application = await _applicationManager.FindByClientIdAsync(request.ClientId, new System.Threading.CancellationToken());

		if (application == null)
		{
			return Forbid(
				authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
				properties: new AuthenticationProperties(
					new Dictionary<string, string>
					{
						[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidClient,
						[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Details concerning the calling client application cannot be found in the database"
					}
				)
			);
		}

		// Flow the request_id to allow OpenIddict to restore
		// the original authorization request from the cache.
		return View(new AuthorizeViewModel
		{
			ApplicationName = application.DisplayName,
			RequestId = request.RequestId,
			Scope = request.Scope
		});
	}

	/// <summary>
	/// Accept an openId request
	/// </summary>
	/// <param name="request"></param>
	/// <returns></returns>
	[Authorize, HttpPost("~/connect/authorize/accept"), ValidateAntiForgeryToken]
	public async Task<IActionResult> Accept()
	{
		var request = HttpContext.GetOpenIddictServerRequest() ??
			throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

		// Retrieve the profile of the logged in user
		var user = await _userManager.GetUserAsync(User);

		if (user == null)
		{
			return Forbid(
				authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
				properties: new AuthenticationProperties(
					new Dictionary<string, string>
					{
						[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ServerError,
						[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "An internal error has occurred"
					}
				)
			);
		}

		// create a new principal
		var principal = await CreatePrincipalAsync(request, user);

		// returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
		return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
	}

	/// <summary>
	/// Deny an openId request
	/// </summary>
	/// <returns></returns>
	[Authorize, HttpPost("~/connect/authorize/deny"), ValidateAntiForgeryToken]
	public IActionResult Deny()
	{
		// Notify OpenIddict that the authorization grant has been denied by the resource owner
		// to redirect the user agent to the client application using the appropriate response_mode.
		return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
	}

	/// <summary>
	/// Logout
	/// </summary>
	/// <returns></returns>
	[HttpPost("~/connect/logout")]
	public async Task<IActionResult> Logout()
	{
		// Ask ASP.NET Core Identity to delete the local and external cookies created
		// when the user agent is redirected from the external identity provider
		// after a successful authentication flow (e.g Google or Facebook).
		await _signInManager.SignOutAsync();

		// Returning a SignOutResult will ask OpenIddict to redirect the user agent
		// to the post_logout_redirect_uri specified by the client application.
		return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
	}

	// Note: to support non-interactive flows like password,
	// you must provide your own token endpoint action:

	/// <summary>
	/// Exchange request for valid openId token
	/// </summary>
	/// <param name="request"></param>
	/// <returns></returns>
	[HttpPost("~/connect/token")]
	[Produces("application/json")]
	public async Task<IActionResult> Exchange()
	{
		var request = HttpContext.GetOpenIddictServerRequest() ??
			throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

		if (request.IsPasswordGrantType())
		{
			var user = await _userManager.FindByNameAsync(request.Username);

			if (user == null)
			{
				return Forbid(
					authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
					properties: new AuthenticationProperties(
						new Dictionary<string, string>
						{
							[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
							[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid"
						}
					)
				);
			}

			// Ensure the user is allowed to sign in.
			if (!await _signInManager.CanSignInAsync(user))
			{
				return Forbid(
					authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
					properties: new AuthenticationProperties(
						new Dictionary<string, string>
						{
							[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
							[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified user is not allowed to sign in"
						}
					)
				);
			}

			// Reject the token request if two-factor authentication has been enabled by the user.
			if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user))
			{
				return Forbid(
					authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
					properties: new AuthenticationProperties(
						new Dictionary<string, string>
						{
							[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
							[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified user is not allowed to sign in"
						}
					)
				);
			}

			// Ensure the user is not already locked out.
			if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))
			{
				return Forbid(
					authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
					properties: new AuthenticationProperties(
						new Dictionary<string, string>
						{
							[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
							[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified user is currently locked out"
						}
					)
				);
			}

			// Ensure that teh user has confirmed their email
			if (_userManager.SupportsUserEmail && !await _userManager.IsEmailConfirmedAsync(user))
			{
				return Forbid(
					authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
					properties: new AuthenticationProperties(
						new Dictionary<string, string>
						{
							[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
							[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified user is not allowed to sign in"
						}
					)
				);
			}

			// Ensure the password is valid.
			if (!await _userManager.CheckPasswordAsync(user, request.Password))
			{
				if (_userManager.SupportsUserLockout)
				{
					await _userManager.AccessFailedAsync(user);
				}

				return Forbid(
					authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
					properties: new AuthenticationProperties(
						new Dictionary<string, string>
						{
							[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
							[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid"
						}
					)
				);
			}

			if (_userManager.SupportsUserLockout)
			{
				await _userManager.ResetAccessFailedCountAsync(user);
			}

			// create a new principal
			var principal = await CreatePrincipalAsync(request, user);

			return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
		}
		else if (request.IsRefreshTokenGrantType())
		{
			// Retrieve the claims principal stored in the authorization code/refresh token.
			var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

			// Retrieve the user profile corresponding to the refresh token
			var user = await _userManager.GetUserAsync(info.Principal);

			if (user == null)
			{
				return Forbid(
					authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
					properties: new AuthenticationProperties(
						new Dictionary<string, string>
						{
							[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
							[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The refresh token is no longer valid"
						}
					)
				);
			}

			// Ensure the user is still allowed to sign in
			if (!await _signInManager.CanSignInAsync(user))
			{
				return Forbid(
					authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
					properties: new AuthenticationProperties(
						new Dictionary<string, string>
						{
							[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
							[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in"
						}
					)
				);
			}

			// create a new principal
			var principal = await CreatePrincipalAsync(request, user);

			return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
		}

		throw new NotImplementedException("The specified grant type is not implemented");
	}

	/// <summary>
	/// Creates a principal based on the openId request
	/// </summary>
	/// <param name="request"></param>
	/// <param name="user"></param>
	/// <param name="properties"></param>
	/// <returns></returns>
	private async Task<ClaimsPrincipal> CreatePrincipalAsync(OpenIddictRequest request, MyUser user, AuthenticationProperties properties = null)
	{
		// Create a new ClaimsPrincipal containing the claims that
		// will be used to create an id_token, a token or a code.
		var principal = await _signInManager.CreateUserPrincipalAsync(user);

		if (!request.IsRefreshTokenGrantType())
		{
			// Set the list of scopes granted to the client application.
			// Note: the offline_access scope must be granted
			// to allow OpenIddict to return a refresh token.
			principal.SetScopes(new[]
			{
			Scopes.OpenId,
			Scopes.Email,
			Scopes.Profile,
			Scopes.OfflineAccess,
			Scopes.Roles
			}.Intersect(request.GetScopes()));
		}

		// Set resource
		principal.SetResources(new string[] { "http://localhost:5000/" });

		// Note: by default, claims are NOT automatically included in the access and identity tokens.
		// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
		// whether they should be included in access tokens, in identity tokens or in both.
		foreach (var claim in principal.Claims)
		{
			claim.SetDestinations(GetDestinations(claim, principal));
		}

		return principal;
	}

	private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
	{
		// Note: by default, claims are NOT automatically included in the access and identity tokens.
		// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
		// whether they should be included in access tokens, in identity tokens or in both.

		switch (claim.Type)
		{
			case Claims.Name:
				yield return Destinations.AccessToken;

				if (principal.HasScope(Scopes.Profile))
					yield return Destinations.IdentityToken;

				yield break;

			case Claims.Email:
				yield return Destinations.AccessToken;

				if (principal.HasScope(Scopes.Email))
					yield return Destinations.IdentityToken;

				yield break;

			case Claims.Role:
				yield return Destinations.AccessToken;

				if (principal.HasScope(Scopes.Roles))
					yield return Destinations.IdentityToken;

				yield break;

			// Never include the security stamp in the access and identity tokens, as it's a secret value.
			case "AspNet.Identity.SecurityStamp": yield break;

			default:
				yield return Destinations.AccessToken;
				yield break;
		}
	}
}

Further technical details

.NET SDK (reflecting any global.json):
 Version:   5.0.101
 Commit:    d05174dc5a

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.19041
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\5.0.101\

Host (useful for support):
  Version: 5.0.1
  Commit:  b02e13abab

.NET SDKs installed:
  3.1.403 [C:\Program Files\dotnet\sdk]
  5.0.100 [C:\Program Files\dotnet\sdk]
  5.0.101 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.All 2.1.23 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.23 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.23 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.1 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.1.9 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.1 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

To install additional .NET runtimes or SDKs:
  https://aka.ms/dotnet-download

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:7 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
Gillardocommented, Dec 11, 2020

@kevinchalet thanks for all this, i will remove that extra code, that should keep things a bit tidier for us especially if not used.

I will use 2 separate certificates for encryption and signing, thanks again for the tip. I dropped the “development” calls because i wondered if that was causing my refresh token issue in the first place.

1reaction
kinosangcommented, Dec 11, 2020

@Gillardo when use rolling refresh tokens, a refresh token cannot be used twice, you will receive a new refresh token when obtain a new access token.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Refresh token expiring (with offline.access scope)
My understanding of the documentation is that the refresh token should be valid for 6 months and can be only used once to...
Read more >
OAuth2: refresh tokens being expired randomly
Recently we are experiencing the following issue: Refresh tokens being expired randomly and unexpectedly, for instance yesterday the refresh ...
Read more >
Refresh token expiring (with offline.access scope) - Twitter API ...
A token object contains both a refresh token and an access token; However, the access token in the object stays valid for 2...
Read more >
Migrate from Renew to Refresh OAuth Tokens
Renewable OAuth access tokens created with older versions of the Square API must be replaced by refreshable tokens before the older tokens expire....
Read more >
Oauth 2.0 refresh token expiration - android
One strategy should be, use access token until it gets expired, after that, use refresh token to get the new access token, if...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found