I am getting the 'Issuer name does not match authority' error because I have an ssl-terminating load balancer in front of my is4 service (i.e. issuer is https://myurl and authority is http://myurl).
What should I do in this situation? The dns names are identical, it is the s in https which is causing the validation failure!
It is possible for your Issuer and Authority to be different, but it requires changes to configuration of the server and the discovery request.
On your Identity Server's startup method, you can set the issuer:
var identityServerBuilder = services.AddIdentityServer(options =>
{
if (Environment.IsDevelopment())
{
options.IssuerUri = $"http://myurl:5000";
}
else
{
options.IssuerUri = $"https://myurl";
}
})
And then in your discovery document request:
DiscoveryDocumentRequest discoveryDocument = null;
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == EnvironmentName.Development)
{
discoveryDocument = new DiscoveryDocumentRequest()
{
Address = "http://myurl:5000",
Policy = {
RequireHttps = false,
Authority = "http://myurl:5000",
ValidateEndpoints = false
},
};
}
else
{
discoveryDocument = new DiscoveryDocumentRequest()
{
Address = "http://myurl:5000",
Policy = {
RequireHttps = false,
Authority = "https://myurl",
ValidateEndpoints = false
},
};
}
var disco = await httpClient.GetDiscoveryDocumentAsync(discoveryDocument);
Your issuer url is https however authority url is http. Both urls should be exactly same.
Else you can try setting ValidateIssuerName property to false. This property decides if issuer name has to be identical to authority or not. By default it is true -
var discoRequest = new DiscoveryDocumentRequest
{
Address = "authority",
Policy = new DiscoveryPolicy
{
ValidateIssuerName = false,
},
};
answer from mackie1001 on identityserver4 gitter
your load balancer should forward on the original protocol (X-Forwarded-Proto) and you can use that to set the current request scheme to match the incoming request
you'd just need to create a middleware function to do it
Have a read of this: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-3.0
for reference this is the code i added to startup:-
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto
});
many thanks mackie1001!
if using nginx as the load balancer you will probably need this in service configuration...
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// Only loopback proxies are allowed by default.
// Clear that restriction because forwarders are enabled by explicit
// configuration.
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
and then add this middleware before the identity server middleware
app.UseForwardedHeaders();
Related
Identity Server is working as expected. I can log user in and log user out. However the PostLogoutRedirectUri property on LogoutRequest object is always coming back as null.
My SPA client configuration:
{
ClientId = "pokemon",
ClientName = "Angular Pokemon Client",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RedirectUris = { "http://localhost:4200/login" },
PostLogoutRedirectUris = { "http://localhost:4200" },
AllowedCorsOrigins = { "http://localhost:4200" },
AllowOfflineAccess = true,
AllowAccessTokensViaBrowser = true,
AllowRememberConsent = false,
RequireConsent = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"scope1"
}
}
The settings for AccountOptions object are:
public static bool AllowLocalLogin = true;
public static bool AllowRememberLogin = true;
/.../
public static bool ShowLogoutPrompt = false;
public static bool AutomaticRedirectAfterSignOut = true;
Then on the client I am using the oidc-client library. I have the following settings configured:
const settings = {
authority: "https://localhost:5001",
client_id: "pokemon",
redirect_uri: "http://localhost:4200/login",
response_type: "code",
scope:"openid profile scope1",
userStore: new WebStorageStateStore({ store: window.localStorage })
}
I have tried with post_logout_redirect_uri value and without. Same result.
The way I make the logout request is this.mgr.signoutRedirect(). I have also tried with adding this.mgr.signoutRedirect({ id_token_hint: user.id_token }) but got same result.
The first request going out of my client to the IdP has the following URL
https://localhost:5001/connect/endsession?id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6IjhEQTE2MDdBRTE2NzJGODQ3RkU2NkE2MUI2NEFGM0IxIiwidHlwIjoiSldUIn0.eyJuYmYiOjE2MDE1ODMxODQsImV4cCI6MTYwMTU4MzQ4NCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMSIsImF1ZCI6InBva2Vtb24iLCJpYXQiOjE2MDE1ODMxODQsImF0X2hhc2giOiJRajc0Z1Z6VGc5WUd3OGVhaTlKWDhRIiwic19oYXNoIjoiM1k2NGtROVFsY2d3Q0VUSGpMT1RDQSIsInNpZCI6IkRFQkFERTA1Njg5RTk1RDY0NUQwNUJGOTkyREJCRTBDIiwic3ViIjoiYWxpY2UiLCJhdXRoX3RpbWUiOjE2MDE1ODMxNjIsImlkcCI6ImxvY2FsIiwiYW1yIjpbInB3ZCJdfQ.xpQo3SFT_Pc4LDtXPHWEETkweLmevUQvPj_84EC98s8qy272mb1dIc3rsIxpHvmBy6f4kI3z4CRs0w6fZmLGyWtZCYCcM6RJhKyGIz_epr-s_ZfZ7XE9Fwvy2FWFZ_HL0SgqLyUCwxKyel0GnzgEmHqcgIbKrK-3KAsVVuNKbXfEwCE-HsVv0OPssAmWvqRdN61ZtbIst4LP6TISkTvlP8HNZozlpbVawGjRPeubyImoYCZgPDVBYI3Ml_xtmSRITdIcTT9S8JmGL4sBIzNXW2ChOTuMvcEkix2lmPH1e9orFA2QOdGgeHylv6sza5ukHR6HTIF9ypoMon-ycNBPJw
Then the second request is fired
https://localhost:5001/Account/Logout?logoutId=CfDJ8CU-F4FvYn9IkMAT1M74c9qWz8pFpIUH_9uKhIkfUFRQKmkVvPVyRNSRpMnTTQ2ZjIqEqFONFzQ6334fLzoKrrUoxjfnIEXYONgXLCnB3IL0OGjaQcP2WIeX-u7UAx_7LIs-DRvGiDEsgnrfhveZknsDPPcJvediQ3viec63gA9EGo5g467Hcd_JClsdikFAd3j2daTxAdVvhmzmjW60ghfibOnsERghDz3FuuX0vDMjBo5JsRyFQeM78BNnvHkoMOIunz2m4RpJLHHzApRxz0Dofl3Oa9JsVxISGevK02Be1W0oTp1eUh_Yb2a6rMYmkhR2vUg4_MazHi61NI5Lvg1X2gn8x3HR2SiKO6-BEiNK07Mt1poyky4A31DcIQiJKQ
On the Identity Server provider, looking at the logs, there are no errors or warning. This code then gets executed:
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await _interaction.GetLogoutContextAsync(logoutId);
var vm = new LoggedOutViewModel
{
AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut,
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
SignOutIframeUrl = logout?.SignOutIFrameUrl,
LogoutId = logoutId
};
and logout?.PostLogoutRedirectUri is always returning null. logoutId has a value. Inspecting source code for GetLogoutContextAsync seems to simply take the logoutId and deserialize it in Message object.
When I manually change PostLogoutRedirectUri to http://localhost:4200 it works. Any ideas why it keeps returning null?
In the logout function on the client side, use the following :-
this._userManager.signoutRedirect({'id_token_hint' : this._user.id_token});
where _user is the returned "User" object. Please refer IdentityServer4 logout as the resolution already exists.
Also, go to the identity server log and check if you get a similar log and observe the call to the session endpoint (the ClientId and the "PostLogOutUri" is null apart from other parameters)
I followed this guide to implement some kind of authentication for my Blazor Web Assembly application. I have an Identity Server 4 instance running on some server, it seems to be completely operational.
My problem is that in the guide above, the returnUrl that is passed to identity server is obviously not a local url. By digging into Identity Server's code, I found that it will always fail to login a user if the return url is not local :
public async Task<AuthorizationRequest> GetAuthorizationContextAsync(string returnUrl)
{
var result = await _returnUrlParser.ParseAsync(returnUrl);
if (result != null)
{
_logger.LogTrace("AuthorizationRequest being returned");
}
else
{
_logger.LogTrace("No AuthorizationRequest being returned");
}
return result;
}
In the code above from DefaultIdentityServerInteractionService, ParseAsync() calls IsLocal() which causes result to be null, which in turn, produces the following in my logs:
2020-09-28T19:37:25.932782009Z [2020-09-28T19:37:25.9324455+00:00] [VRB] [] [IdentityServer4.Services.OidcReturnUrlParser] returnUrl is not valid
2020-09-28T19:37:25.932807561Z [2020-09-28T19:37:25.9325314+00:00] [VRB] [] [IdentityServer4.Services.OidcReturnUrlParser] No AuthorizationRequest being returned
2020-09-28T19:37:25.932817324Z [2020-09-28T19:37:25.9325559+00:00] [VRB] [] [IdentityServer4.Services.DefaultIdentityServerInteractionService] No AuthorizationRequest being returned
Could someone point me towards what I'm not understanding here ? Can I provide any more information ?
I'm not sure if your issue is about LocalUrl, it seems that you set it wrong if you followed the guid.
If you are following default settings, its enough to have client config on IDS4 like this:
new Client
{
ClientId = "wasmappauth-client",
ClientName = "Blazor Webassembly App Client",
RequireClientSecret = false,
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
AllowedCorsOrigins = { "http://localhost:5005" },
RedirectUris = { "http://localhost:5005/authentication/login-callback" },
PostLogoutRedirectUris = { "http://localhost:5005/authentication/logout-callback" },
AllowedScopes = {"openid", "profile"},
}
Here is my blog post which I explained same thing but in simpler wording: https://nahidfa.com/posts/blazor-webassembly-authentication-and-authorization-with-identityserver4/
Edit: About check for local URLs, its it by design on IDS4
I'm using .NET Core 3.1 with Identity Server 4 and connecting to Azure AD via OpenIdConnect. I'm using a Vue.js front-end and .NET Core API. IdentityServer, the front-end, and the API are all hosted on-prem on the same server (same domain). Everything uses https. I'm using an Oracle database with EF model first, with fully-customized IdentityServer stores and a custom user store (I implemented the interfaces). I'm using IdentityServer's Quickstart, edited a little to hook up my custom user store instead of the test user. I'm running this in my dev environment.
If I type in the url to the IdentityServer, I'm redirected to Azure AD, signed-in successfully, and shown this page:
Grants - successful login
The claims are coming back from Azure AD and the auto-provisioning is successful. It is written successfully to the database.
Authenticating through my JS client hits IdentityServer, redirects to Azure AD, I sign-in, then it redirects to IdentityServer's ExternalController, then redirects back to a Microsoft url, then proceeds to repeat until it finally fails with this page:
Sign-in failure from Azure AD
My guess is I messed up a redirect uri somewhere. Here is my code and the IdentityServer log:
IdentityServer Log
That block of logging repeats 6-10 times. No errors or anything different at the end.
I had to break up the C# code because the site couldn't handle one of my long options lines.
IdentityServer Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.UserInteraction.LoginUrl = "/Account/Login";
options.UserInteraction.LogoutUrl = "/Account/Logout";
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10),
CookieSlidingExpiration = true
};
}).AddClientStore<ClientStore>()
.AddCorsPolicyService<CorsPolicyService>()
.AddResourceStore<ResourceStore>()
.AddPersistedGrantStore<PersistedGrantStore>()
.AddProfileService<UserProfileService>();
services.AddScoped<IUserStore, UserStore>();
if (env.IsDevelopment())
{
// not recommended for production
builder.AddDeveloperSigningCredential();
}
else
{
// TODO: Load Signing Credentials for Production.
}
services.AddAuthentication()
.AddOpenIdConnect("aad", "Azure AD", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://login.windows.net/[authority]";
options.CallbackPath = "/callback-aad";
options.ClientId = "[ClientId]";
options.RemoteSignOutPath = "/signout-aad";
options.RequireHttpsMetadata = true;
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.SaveTokens = true;
options.SignedOutCallbackPath = "/signout-callback-aad";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
options.UsePkce = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseStaticFiles();
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Client OIDC config:
const oidcSettings = {
authority: '[IdentityServerUrl]',
client_id: '[ClientId]',
post_logout_redirect_uri: '[front-end url]/logout-aad',
redirect_uri: '[front-end url]/callback-aad',
response_type: 'code',
save_tokens: true,
scope: 'openid profile',
}
Callback method being hit for ExternalController:
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
if (_logger.IsEnabled(LogLevel.Debug))
{
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {#claims}", externalClaims);
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = await AutoProvisionUser(provider, providerUserId, claims);
}
// this allows us to collect any additional claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId));
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", returnUrl);
}
}
return Redirect(returnUrl);
}
Azure AD config:
redirect uri: [IdentityServer url]/callback-aad
Database table data:
Client table IMG1
Client table IMG2
ClientScopes table
ClientRedirectUris table
Please let me know if you need any additional information. Thank you
The problem was in my custom UserStore. I was getting the user by the Azure AD SubjectId instead of the UserSubjectId. So in the ExternalController, the ApplicationUser object was coming up as null. Instead of an exception, it kept going back to Azure AD to try to get the user again, but obviously that just creates an infinite loop. I didn't think to look there since my user was successfully provisioned with Id's and claims.
I have a new IdP that implements IdentityServer4 (.NET Core). I am using it to provide SSO/Cookie authentication/authorization to an MVC5 client app. Since the client app is not .NET Core, I use the IdentityServer3 and Microsoft.Owin nugets in order to integrate. There aren't tons of examples of mixing .NET Core and .NET together like this, but there are a few and I've done my best to make it work. Here is the configuration source code for each:
IdentityServer4 (.NET Core, based largely on this example):
new Client()
{
ClientId = "myClientId",
ClientName = "My Client",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
Enabled = true,
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
}
}
My Client (MVC5):
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "http://localhost:5000",
ClientId = "myClientId",
RedirectUri = "http://localhost:5002/signin-oidc",
PostLogoutRedirectUri = "http://localhost:5002/signout-callback-oidc",
ResponseType = "code id_token",
SignInAsAuthenticationType = "Cookies",
Scope = "openid",
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = n => {
n.OwinContext.Response.Cookies.Append("stored_id_token", n.ProtocolMessage.IdToken);
return Task.FromResult(0);
},
RedirectToIdentityProvider = n => {
if (n.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.Request.Cookies["stored_id_token"];
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint;
var signOutMessageId = n.OwinContext.Environment.GetSignOutMessageId(); // returns NULL!
//var signOutMessageId = n.OwinContext.Request.Query.Get("id"); // same thing as line above
if (signOutMessageId != null)
{
n.ProtocolMessage.State = signOutMessageId;
}
}
}
return Task.FromResult(0);
}
}
});
Logout mostly works ok. The issue I have is with redirecting back to the login page so that the enduser can login again to the client they just logged out of via the link highlighted here:
The RedirectUri is configured correctly on both sides of configuration (IdP and Client), and the hyperlink's href value is set in the View (pictured), but that URL also needs a query param called "state" attached to it so that it knows which application to log back into (eg. http://localhost:5002/signout-callback-oidc?state=123456789 Clicking this link without the "state" param gives you a 404.). The issue I'm having is that I'm unable to get/set this "state" value in "My Client".
From everything I've read, you get it by calling n.OwinContext.Environment.GetSignOutMessageId() which is actually just calling n.OwinContext.Request.Query.Get("id") under the covers, but it always returns null. Any idea why this returns null??
Thank you!
I'm using Asp.net core rc2 with OpenIdConnectServer. I'm using angular 1.x with augular-oauth2. After a few days, my error has digressed to
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:54275/api/Account/Username
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: Successfully validated the token.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: HttpContext.User merged via AutomaticAuthentication from authenticationScheme: Bearer.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: AuthenticationScheme: Bearer was successfully authenticated.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed for user: .
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Warning: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes (Bearer).
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: AuthenticationScheme: Bearer was forbidden.
My ConfigureServices consists of
services.AddAuthorization(options =>
{
options.AddPolicy("UsersOnly", policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim("role");
});
});
My configure has
app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), branch =>
{
branch.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
RequireHttpsMetadata = false,
Audience = "http://localhost:54275/",
Authority = "http://localhost:54275/",
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = "client1",
//ValidAudiences = new List<string> { "", "empty", "null"}
}
});
});
app.UseOpenIdConnectServer(options =>
{
options.AuthenticationScheme = OpenIdConnectServerDefaults.AuthenticationScheme;
options.Provider = new SimpleAuthorizationServerProvider();
options.AccessTokenHandler = new JwtSecurityTokenHandler();
options.ApplicationCanDisplayErrors = true;
options.AllowInsecureHttp = true;
options.TokenEndpointPath = new PathString("/oauth2/token");
options.LogoutEndpointPath = new PathString("/oauth2/logout");
options.RevocationEndpointPath = new PathString("/oauth2/revoke");
options.UseJwtTokens();
//options.AccessTokenLifetime = TimeSpan.FromHours(1);
});
My authorize attribute is defined on the Controller as
[Authorize(Policy = "UsersOnly", ActiveAuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme), Route("api/Account")]
I store the token as a cookie and attach it to requests using an http interceptor in angular.
I generate the token with
public override async Task GrantResourceOwnerCredentials(GrantResourceOwnerCredentialsContext context)
{
// validate user credentials (demo mode)
// should be stored securely (salted, hashed, iterated)
using (var con = new SqlConnection(ConnectionManager.GetDefaultConnectionString()))
{
if (!Hashing.ValidatePassword(context.Password, await con.ExecuteScalarAsync<string>("SELECT PassHash FROM dbo.Users WHERE Username = #UserName", new { context.UserName })))
{
context.Reject(
error: "bad_userpass",
description: "UserName/Password combination was invalid."
);
return;
}
// create identity
var id = new ClaimsIdentity(context.Options.AuthenticationScheme);
id.AddClaim(new Claim("sub", context.UserName));
id.AddClaim(new Claim("role", "user"));
// create metadata to pass on to refresh token provider
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{"as:client_id", context.ClientId}
});
var ticket = new AuthenticationTicket(new ClaimsPrincipal(id), props,
context.Options.AuthenticationScheme);
ticket.SetAudiences("client1");
//ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.Email, OpenIdConnectConstants.Scopes.Profile, "api-resource-controller");
context.Validate(ticket);
}
}
I've spent the last three days on this problem and I realize that at this point I'm probably missing something obvious due to lack of sleep. Any help would be appreciated.
The error you're seeing is likely caused by 2 factors:
You're not attaching an explicit destination to your custom role claim so it will never be serialized in the access token. You can find more information about this security feature on this other SO post.
policy.RequireClaim("role"); might not work OTB, as IdentityModel uses an internal mapping that converts well-known JWT claims to their ClaimTypes equivalent: here, role will be likely replaced by http://schemas.microsoft.com/ws/2008/06/identity/claims/role (ClaimTypes.Role). I'd recommend using policy.RequireRole("user") instead.
It's also worth noting that manually storing the client_id is not necessary as it's already done for you by the OpenID Connect server middleware.
You can retrieve it using ticket.GetPresenters(), that returns the list of authorized presenters (here, the client identifier). Note that it also automatically ensures a refresh token issued to a client A can't be used by a client B, so you don't have to do this check in your own code.