How to limit an aspnet core Action for specific Roles with Auth0 and JWT - jwt

I'm creating a PWA with React and using Auth0 as my identity provider and JWT Bearer token as authentication. I am injecting roles into my JWT token so that the client-side can limit what options are available to the user and I have this working pretty well.
I want to now limit the server side so that an endpoint can't be called unless the user has the necessary role(s) required to access that endpoint.
Annoyingly, Auth0 doesn't appear to support adding in the roles or role claim that aspnet core seems to handle OOTB; it requires that a domain preface the roles in the claims definition. ie, https://bob.com/roles as the claim.
I'm trying to work out how to get the Authorize(Roles = "Administrator") attribute to honour the domain-prefaced claim for roles.
I have tried updating the Auth0 Rule to set the role or roles property but these never get returned; only the domain-prefaced roles claim seems to return.
I have found other info for more specific Authentication providers and they include a MapJsonKey extension on ClaimActions that look like they would fit the bill, but the standard AuthenticationOptions object in the AddAuthentication extension doesn't appear to have this.
My ConfigureServices in App.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// 1. Add Authentication Services
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = Configuration["Auth0:Authority"];
options.Audience = Configuration["Auth0:ClientId"];
});
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
My Rule for injecting the roles into the JWT in Auth0:
function (user, context, callback) {
const namespace = 'http://bob.com';
const assignedRoles = (context.authorization || {}).roles;
let idTokenClaims = context.idToken || {};
let accessTokenClaims = context.accessToken || {};
idTokenClaims[`roles`] = assignedRoles; // This was an attempt to set the roles in 'roles' but doesn't get returned.
accessTokenClaims[`roles`] = assignedRoles;
idTokenClaims[`${namespace}/roles`] = assignedRoles; // This does get returned
accessTokenClaims[`${namespace}/roles`] = assignedRoles;
context.idToken = idTokenClaims;
context.accessToken = accessTokenClaims;
callback(null, user, context);
}
Example JWT Payload
{
"http://bob.com/roles": [
"Administrator"
],
"given_name": "Name",
"iss": "{issuer}",
"sub": "{subject}",
"aud": "{audience}"
}
asp.net core Action (taken from the example project, but with auth added)
[HttpGet("[action]"), Authorize(Roles = "Administrator")]
public IEnumerable<WeatherForecast> WeatherForecasts()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
DateFormatted = DateTime.Now.AddDays(index).ToString("d"),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
});
}
What I would like is to be able to either map the http://bob.com/roles to roles, get the aspnet core Authorize attribute to look at the http://bob.com/roles, or get Auth0 to be able to return the roles in a 'roles' object.
Where I got the MapJsonKey info from:
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/additional-claims?view=aspnetcore-2.2
Using Roles with the ASP.NET Core JWT middleware
https://www.jerriepelser.com/blog/using-roles-with-the-jwt-middleware/

For anyone who finds this, I found a solution to this. If you update the JWT claim to be http://schemas.microsoft.com/ws/2008/06/identity/claims/role then it all works straight away.
Updated Auth0 Rule
function (user, context, callback) {
const assignedRoles = (context.authorization || {}).roles;
let idTokenClaims = context.idToken || {};
let accessTokenClaims = context.accessToken || {};
idTokenClaims[`http://schemas.microsoft.com/ws/2008/06/identity/claims/role`] = assignedRoles;
accessTokenClaims[`http://schemas.microsoft.com/ws/2008/06/identity/claims/role`] = assignedRoles;
context.idToken = idTokenClaims;
context.accessToken = accessTokenClaims;
callback(null, user, context);
}

Related

ITfoxtec.Identity.Saml2 - Multiple authentication schemes with Asp.net Core Identity

I am aware of the answer given here which is about using Forms Authentication & SAML. In My case I am using Asp.net core Identity on .Net 5. Also I am using two authentication schemes (Cookies & JWT).
My auth pipeline goes as;
//include identity
services.AddIdentity<ApplicationUser, ApplicationRole>(SetupIdentityOptions)
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
//configure cookie and Jwt scheme
services.ConfigureApplicationCookie(...)
services.AddAuthentication(...) //configures default Identity
.AddJwtBearer(options => {...})
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
What I want to know is where should I add SAML2 in this pipeline.
In general the application should;
Be able to login with Cookie authentication (Identity takes care of this)
Jwt tokens should work as well for Apis (this also works with Jwt scheme)
SSO clients should get authenticated from their IdP and when redirected to AssertionConsumer() I will create additional claims create new ClaimsIdentity, create a JWT token(pass it to client) and get the user to dashboard.
I am stuck at 3rd point which is to properly add the SAML scheme without overriding cookie/jwt schemes.
The error No sign-in authentication handler is registered for the scheme 'saml2' probably occurs bedause you need to add services.AddSaml2() and app.UseSaml2()
You can use the setup from the provided example project. Newly added middleware should not interfere with what you already have.
When a SAML2 IdP redirects back to your application, you are given a result that identifies the authenticated user, e.g. Email Address or SSN (in case it is a government Id Provider).
You can combine that information with a Role (e.g. SpecialCustomer, Citizen, or an existing Role that you already have) into a cookie or JWT Token as you probably already do for other users. This can always be distinguished from other cookies and tokens by the Role. Regular [Authorize(....)] attributes will also work just fine.
I was stuck at the same point.. The solution I found:
If you check the source code of IFOXTEC.IDENTITY.SAML2, the method AddSaml2 overrides your AddAuthentication method and adds the AddCookie section.
public static IServiceCollection AddSaml2(this IServiceCollection services, string loginPath = "/Auth/Login", bool slidingExpiration = false, string accessDeniedPath = null, ITicketStore sessionStore = null, SameSiteMode cookieSameSite = SameSiteMode.Lax, string cookieDomain = null, CookieSecurePolicy cookieSecurePolicy = CookieSecurePolicy.SameAsRequest)
{
services.AddAuthentication(Saml2Constants.AuthenticationScheme)
.AddCookie(Saml2Constants.AuthenticationScheme, o =>
{
o.LoginPath = new PathString(loginPath);
o.SlidingExpiration = slidingExpiration;
if(!string.IsNullOrEmpty(accessDeniedPath))
{
o.AccessDeniedPath = new PathString(accessDeniedPath);
}
if (sessionStore != null)
{
o.SessionStore = sessionStore;
}
o.Cookie.SameSite = cookieSameSite;
o.Cookie.SecurePolicy = cookieSecurePolicy;
if (!string.IsNullOrEmpty(cookieDomain))
{
o.Cookie.Domain = cookieDomain;
}
});
return services;
}
So, to add SAML to your pipeline, you can remove the services.AddSaml2(), add the AddCookie section and, inside your policy, you can add the verification of any cookie with name saml2 to forward to your SAML schema.
services
.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = "custom-schema";
sharedOptions.DefaultChallengeScheme = "custom-schema";
})
.AddPolicyScheme("custom-schema", null, options =>
{
options.ForwardDefaultSelector = context =>
{
if (context.Request.Headers["Authorization"].Any(x => x.StartsWith("Bearer ")))
return JwtBearerDefaults.AuthenticationScheme;
else if (context.Request.Headers["Cookie"].Any(x => x.Contains(".saml2=")))
return Saml2Constants.AuthenticationScheme;
return "Identity.Application";
};
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, null, options =>
{
//...
})
.AddCookie(Saml2Constants.AuthenticationScheme, o =>
{
o.LoginPath = new PathString("/Auth/Login");
o.SlidingExpiration = false;
o.Cookie.SameSite = SameSiteMode.Lax;
o.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});

In identity server 3 how to relate user to client

I am using Identity Server 3. I have couple applications ie. Client configured and have few users configured. How do i establish the relationship between User and a Client and also view all applications that the selected User has access to.
Update 1
I am sorry if question was confusing. On IdSvr3 home page, there is a link to revoke application permissions. I am guessing in order to revoke the permission you have to first establish the relationship between user and application.
and i wanted to know how to establish that permission when i add new user?
There's no direct way to limit one or multiple users to a certain client. This is where you should think about implementing your own custom validation. Fortunately, the IdentityServer provides an extensibility point for this kind of requirement.
ICustomRequestValidator
You should implement this interface to further validate users to see if they belong to certain clients and filter them out. You can look into the user details by looking at ValidatedAuthorizeRequest.Subject. This custom validator will start after validating optional parameters such as nonce, prompt, arc_values ( AuthenticationContextReference ), login_hint, and etc. The endpoint is AuthorizeEndPointController and the default implementation of the interface for the tailored job is AuthorizeRequestValidator and its RunValidationAsync. You should take a look at the controller and the class.
Implementation tip
By the time the custom request validation begins, a Client reference will be presented in ValidatedAuthorizeRequest. So all you need to do would be matching the client id or some other identifiers you think you need to verify the client. Probably, you might want to add a Claim key-value pair to your client which you want to allow a few users.
Maybe something like this.
new InMemoryUser{Subject = "870805", Username = "damon", Password = "damon",
Claims = new Claim[]
{
new Claim(Constants.ClaimTypes.Name, "Damon Jeong"),
new Claim(Constants.ClaimTypes.Email, "dmjeong#email.com"),
new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean)
}
}
Assume you have above user, then add the subject id to the claim of a client like below.
new Client
{
ClientName = "WPF WebView Client Sample",
ClientId = "wpf.webview.client",
Flow = Flows.Implicit,
.
.
.
// Add claim for limiting this client to certain users.
// Since a claim only accepts type and value as string,
// You can add a list of subject id by comma separated values
// eg ( new Claim("BelongsToThisUser", "870805, 870806, 870807") )
Claims = new List<Claim>
{
new Claim("BelongsToThisUser", "870805")
}
},
And then just implement the ICustomRequestValidator and try to match the Claim value with the given user in its ValidateAuthorizeRequestAsync.
public class UserRequestLimitor : ICustomRequestValidator
{
public Task<AuthorizeRequestValidationResult> ValidateAuthorizeRequestAsync(ValidatedAuthorizeRequest request)
{
var clientClaim = request.Client.claims.Where(x => x.Type == "BelongsToThisUser").FirstOrDefault();
// Check is this client has "BelongsToThisUser" claim.
if(clientClaim != null)
{
var subClaim = request.Subject.Claims.Where(x => x.Type == "sub").FirstOrDefault() ?? new Claim(string.Empty, string.Empty);
if(clientClaim.Value == userClaim.Value)
{
return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult
{
IsError = false
});
}
else
{
return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult
{
ErrorDescription = "This client doesn't have an authorization to request a token for this user.",
IsError = true
});
}
}
// This client has no access controls over users.
else
{
return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult
{
IsError = false
});
}
}
public Task<TokenRequestValidationResult> ValidateTokenRequestAsync(ValidatedTokenRequest request)
{
// your implementation
}
}
Time to DI
You need to inject your own dependency when you configure up your IdentityServer. The authorization server uses IdentityServerServiceFactory for registering dependencies.
var factory = new IdentityServerServiceFactory();
factory.Register(new Registration<ICustomRequestValidator>(resolver => new UserRequestLimitor()));
Then Autofac; the IoC container in IdentityServer will do the rest of the DI jobs for you.

AppSync: Get user information in $context when using AWS_IAM auth

In AppSync, when you use Cognito User Pools as your auth setting your identity you get
identity:
{ sub: 'bcb5cd53-315a-40df-a41b-1db02a4c1bd9',
issuer: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_oicu812',
username: 'skillet',
claims:
{ sub: 'bcb5cd53-315a-40df-a41b-1db02a4c1bd9',
aud: '7re1oap5fhm3ngpje9r81vgpoe',
email_verified: true,
event_id: 'bb65ba5d-4689-11e8-bee7-2d0da8da81ab',
token_use: 'id',
auth_time: 1524441800,
iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_oicu812',
'cognito:username': 'skillet',
exp: 1524459387,
iat: 1524455787,
email: 'myemail#nope.com' },
sourceIp: [ '11.222.33.200' ],
defaultAuthStrategy: 'ALLOW',
groups: null }
However when you use AWS_IAM auth you get
identity:
{ accountId: '12121212121', //<--- my amazon account ID
cognitoIdentityPoolId: 'us-west-2:39b1f3e4-330e-40f6-b738-266682302b59',
cognitoIdentityId: 'us-west-2:a458498b-b1ac-46c1-9c5e-bf932bad0d95',
sourceIp: [ '33.222.11.200' ],
username: 'AROAJGBZT5A433EVW6O3Q:CognitoIdentityCredentials',
userArn: 'arn:aws:sts::454227793445:assumed-role/MEMORYCARDS-CognitoAuthorizedRole-dev/CognitoIdentityCredentials',
cognitoIdentityAuthType: 'authenticated',
cognitoIdentityAuthProvider: '"cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob","cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob:CognitoSignIn:1a072f08-5c61-4c89-807e-417d22702eb7"' }
The Docs says that this is expected, https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html .
However, if you use AWS_IAM connected to Cognito (which is required to have unauthenticated access), how are you supposed to get at the User's username, email, sub, etc? I need access to the user's claims when using AWS_IAM type Auth.
For making User's username, email, sub etc. accessible through AppSync API, there's an answer for that: https://stackoverflow.com/a/42405528/1207523
To sum it up, you want to send User Pools ID token to your API (e.g. AppSync or API Gateway). Your API request is IAM authenticated. Then you validate the ID token in a Lambda function and now you have your validated IAM user and User Pools data together.
You want to use the IAM's identity.cognitoIdentityId as primary key for you User table. Add the data included in ID token (username, email, etc.) as attributes.
This way you can make user's claims available through you API. Now, for example, you can set $ctx.identity.cognitoIdentityId as the owner of an item. Then maybe other users can see the name of the owner via GraphQL resolvers.
If you need to access the user's claims in your resolver I'm afraid that doesn't seems to be possible at the moment. I have made a question about this as it would be very helpful for authorization: Group authorization in AppSync using IAM authentication
In this case, instead of using a resolver you could use Lambda as a data source and retrieve the user's claims from the above-mentioned User table.
It's all a bit difficult at the moment :)
Here is bad answer that works. I notice that cognitoIdentityAuthProvider: '"cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob","cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob:CognitoSignIn:1a072f08-5c61-4c89-807e-417d22702eb7" contains the Cognito user's sub (the big after CognitoSignIn). You can extract that with a regex and use the aws-sdk to get the user's info from cognito user pool.
///////RETRIEVE THE AUTHENTICATED USER'S INFORMATION//////////
if(event.context.identity.cognitoIdentityAuthType === 'authenticated'){
let cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
//Extract the user's sub (ID) from one of the context indentity fields
//the REGEX in match looks for the strings btwn 'CognitoSignIn:' and '"', which represents the user sub
let userSub = event.context.identity.cognitoIdentityAuthProvider.match(/CognitoSignIn:(.*?)"/)[1];
let filter = 'sub = \"'+userSub+'\"' // string with format = 'sub = \"1a072f08-5c61-4c89-807e-417d22702eb7\"'
let usersData = await cognitoidentityserviceprovider.listUsers( {Filter: filter, UserPoolId: "us-west-2_KsyTKrQ2M",Limit: 1}).promise()
event.context.identity.user=usersData.Users[0];
}
It's a bad answer because you are pinging the User Pool database instead of just decoding a JWT.
Here is my answer. There was a bug in the appSync client library that would overwrite all custom headers. That has since been fixed. Now you can pass down custom headers that will make it all the way to you resolvers, which I pass to my lambda functions (again, note I am using lambda datasourcres and not using dynamoDB).
So I attach my logged in JWT on the client side and, server side in my lambda function, I decode it. You need the public key created by cognito to validate the JWT. (YOU DO NOT NEED A SECRET KEY.) There is a "well known key" url associated with every user pool which I ping the first time my lambda is spun up but, just like my mongoDB connection, it is persisted between lambda calls (at least for a while.)
Here is lambda resolver...
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const jwkToPem = require('jwk-to-pem');
const request = require('request-promise-native');
const _ = require('lodash')
//ITEMS THAT SHOULD BE PERSISTED BETWEEN LAMBDA EXECUTIONS
let conn = null; //MONGODB CONNECTION
let pem = null; //PROCESSED JWT PUBLIC KEY FOR OUR COGNITO USER POOL, SAME FOR EVERY USER
exports.graphqlHandler = async (event, lambdaContext) => {
// Make sure to add this so you can re-use `conn` between function calls.
// See https://www.mongodb.com/blog/post/serverless-development-with-nodejs-aws-lambda-mongodb-atlas
lambdaContext.callbackWaitsForEmptyEventLoop = false;
try{
////////////////// AUTHORIZATION/USER INFO /////////////////////////
//ADD USER INFO, IF A LOGGED IN USER WITH VALID JWT MAKES THE REQUEST
var token = _.get(event,'context.request.headers.jwt'); //equivalen to "token = event.context.re; quest.headers.alexauthorization;" but fails gracefully
if(token){
//GET THE ID OF THE PUBLIC KEY (KID) FROM THE TOKEN HEADER
var decodedToken = jwt.decode(token, {complete: true});
// GET THE PUBLIC KEY TO NEEDED TO VERIFY THE SIGNATURE (no private/secret key needed)
if(!pem){
await request({ //blocking, waits for public key if you don't already have it
uri:`https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`,
resolveWithFullResponse: true //Otherwise only the responce body would be returned
})
.then(function ( resp) {
if(resp.statusCode != 200){
throw new Error(resp.statusCode,`Request of JWT key with unexpected statusCode: expecting 200, received ${resp.statusCode}`);
}
let {body} = resp; //GET THE REPSONCE BODY
body = JSON.parse(body); //body is a string, convert it to JSON
// body is an array of more than one JW keys. User the key id in the JWT header to select the correct key object
var keyObject = _.find(body.keys,{"kid":decodedToken.header.kid});
pem = jwkToPem(keyObject);//convert jwk to pem
});
}
//VERIFY THE JWT SIGNATURE. IF THE SIGNATURE IS VALID, THEN ADD THE JWT TO THE IDENTITY OBJECT.
jwt.verify(token, pem, function(error, decoded) {//not async
if(error){
console.error(error);
throw new Error(401,error);
}
event.context.identity.user=decoded;
});
}
return run(event)
} catch (error) {//catch all errors and return them in an orderly manner
console.error(error);
throw new Error(error);
}
};
//async/await keywords used for asynchronous calls to prevent lambda function from returning before mongodb interactions return
async function run(event) {
// `conn` is in the global scope, Lambda may retain it between function calls thanks to `callbackWaitsForEmptyEventLoop`.
if (conn == null) {
//connect asyncoronously to mongodb
conn = await mongoose.createConnection(process.env.MONGO_URL);
//define the mongoose Schema
let mySchema = new mongoose.Schema({
///my mongoose schem
});
mySchema('toJSON', { virtuals: true }); //will include both id and _id
conn.model('mySchema', mySchema );
}
//Get the mongoose Model from the Schema
let mod = conn.model('mySchema');
switch(event.field) {
case "getOne": {
return mod.findById(event.context.arguments.id);
} break;
case "getAll": {
return mod.find()
} break;
default: {
throw new Error ("Lambda handler error: Unknown field, unable to resolve " + event.field);
} break;
}
}
This is WAY better than my other "bad" answer because you are not always querying a DB to get info that you already have on the client side. About 3x faster in my experience.
If you are using AWS Amplify, what I did to get around this was to set a custom header username as explained here, like so:
Amplify.configure({
API: {
graphql_headers: async () => ({
// 'My-Custom-Header': 'my value'
username: 'myUsername'
})
}
});
then in my resolver I would have access to the header with:
$context.request.headers.username
As explained by the AppSync's docs here in the section Access Request Headers
Based on Honkskillets answer, I have written a lambda function that will return you the user attributes. You just supply the function with the JWT.
const jwt = require("jsonwebtoken");
const jwkToPem = require("jwk-to-pem");
const request = require("request-promise");
exports.handler = async (event, context) => {
try {
const { token } = event;
const decodedToken = jwt.decode(token, { complete: true });
const publicJWT = await request(
`https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`
);
const keyObject = JSON.parse(publicJWT).keys.find(
key => key.kid == decodedToken.header.kid
);
const pem = jwkToPem(keyObject);
return {
statusCode: 200,
body: jwt.verify(token, pem)
};
} catch (error) {
console.error(error);
return {
statusCode: 500,
body: error.message
};
}
};
I use it in Appsync where I create Pipeline resolvers and add this function whenever I need user attributes. I supply the JWT by grabbing it from the header in the resolver using $context.request.

Identity Server and Access Token Claims

I'm using identity server 3 with windows authentication and adding claims to user's token. I noticed GetProfileDataAsync is called twice which the callers are "ClaimsProviderAccessToken" which doesn't have any requested claims and "ClaimsProviderIdentityToken" is the caller which does. How do I get the RequestedClaimTypes such as Role, Email, whatever in the "ClaimsProviderAccessToken" ??
public override Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// issue the claims for the user
var user = Users.SingleOrDefault(x => x.Subject == context.Subject.GetSubjectId());
if (user != null && context.RequestedClaimTypes != null)
{
context.IssuedClaims = user.Claims.Where(x => context.RequestedClaimTypes.Contains(x.Type));
}
//NOTE: Uncomment and all the claims I need are in access token ?? Comment out and no claims in Access Token ??
//context.IssuedClaims = user.Claims;
return Task.FromResult(0);
}
Here's my scope claim that is requesting the claims to be in access token:
new Scope
{
Name = "api",
Enabled = true,
DisplayName = "Sample API",
Description = "Access to a simple API",
Type= ScopeType.Resource,
IncludeAllClaimsForUser = true,
Claims = new List<ScopeClaim>
{
new ScopeClaim(Constants.ClaimTypes.Name),
new ScopeClaim(Constants.ClaimTypes.Role),
new ScopeClaim(Constants.ClaimTypes.Email),
},
ScopeSecrets = new List<Secret>
{
new Secret("api-secret".Sha256())
}
}
Am I missing something or is it correct to just set the context.IssuedClaims to the user.Claims or should I file by RequestedClaimTypes?? I'm really lost a little trying to figure how this works and not sure if setting context.IssuedClaims = user.Claims although this seems like the behavior I need ???
I actually found the answer, setting the IncludeAllClaimsForUser = true clears out the claims, once I removed that the context.RequestedClaimsTypes are not null when requesting the access token.

IdentityServer3 No scopeclaims when using reference token

In IdentityServer I've added a new scope like this:
new Scope
{
Name = "myscope",
Description = "myscope",
Type=ScopeType.Resource,
ShowInDiscoveryDocument= false,
Emphasize = false,
//AccessTokenType=1, //Reference
AccessTokenType=0, //JWT
Claims = new List<ScopeClaim>
{
new ScopeClaim("location"),
}
I've added a client:
new Client
{
ClientName = "myclient",
Enabled = true,
ClientId = "myclient",
Flow = Flows.Implicit,
AllowedScopes = new List<string> {"myscope"},
Claims = new List<Claim> {new Claim("location", "datacenter")}
}
I've added an implementation of GetProfileData :
public override async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
await base.GetProfileDataAsync(context);
if (context.AllClaimsRequested)
context.IssuedClaims = context.Subject.Claims;
else if (context.RequestedClaimTypes != null)
context.IssuedClaims = context.Subject.Claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
}
In my webapi, I'm using AccessTokenValidation:
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:5300",
AllowedScopes = { "myscope" },
RequireHttpsMetadata = false,
});
services.AddAuthorization(options =>
{
options.AddPolicy("location", policy => policy.RequireClaim("location"));
});
My controller is prefixed with:
[Authorize(ActiveAuthenticationSchemes = "Bearer", Policy = "location")]
public async Task<IActionResult> Get()
{
...
}
Now, when the accesstoken is set to JWT, this works fine, I'm able to call the endpoint. Now, if I change AccessTokenType to reference token, it fails...
If I inspect the RequestedClaimTypes during the call to the profiledata endpoint, it holds the claims for 'myscope' when using JWT, but not when using Reference Token...
Am I missing some configuration or is this the way it's supposed work?? I would have expected to get the same claims in both setup
Reference Tokens are not self-contained tokens like JWTs are. They provide an ID that can be used to fetch the information that the reference token represents from a backing store.
If you're using IdentityServer3 out of the box, you should be able to request your reference token from the POST /connect/token endpoint and follow that up with a request to the token introspection endpoint:
POST /connect/accesstokenvalidation
token={tokenReceivedFromPreviousRequest}
This will return the information for that reference token that is kept in its backing store, including scopes.
As a note, that introspection endpoint accepts both Reference Tokens and JWTs.