K
Kinde6mo ago
LIFE

Resolving auth in back-end (.NET) with token retrieved from front-end (React)

Hello, i have finished a setup in react, but i need to use the token to authenticate and authorize the user in the back-end. AFAIK there are no documents on Kinde elaborating on this issue, would anyone be able to support? In .NET authentication is added:
var jwtIssuer = builder.Configuration.GetSection("Jwt:Issuer").Get<string>();

builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = jwtIssuer,
};
});
var jwtIssuer = builder.Configuration.GetSection("Jwt:Issuer").Get<string>();

builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = jwtIssuer,
};
});
but the access_token is always null
[HttpGet]
public async Task<IActionResult> C()
{
var a = HttpContext.User;
var accessToken = await HttpContext.GetTokenAsync("access_token");
return Ok("test B");
}
[HttpGet]
public async Task<IActionResult> C()
{
var a = HttpContext.User;
var accessToken = await HttpContext.GetTokenAsync("access_token");
return Ok("test B");
}
36 Replies
jacquesy
jacquesy6mo ago
Hi @LIFE, Do you need the actual access token in your controller action, or do you just need the details of the authenticated user? Here's how I validate and use the access token (provided in the Authorization header in the request from my React app) - In startup.cs:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = Configuration["Auth:Authority"];
options.Audience = Configuration["Auth:Audience"];
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = Configuration["Auth:Authority"];
options.Audience = Configuration["Auth:Audience"];
});
Now in your controller action you should be able to see HttpContext.User populated with properties for the authenticated user - their userId (the sub claim), their Kinde organization (the org_code claim) etc
LIFE
LIFE6mo ago
Aha, Thanks! No I do not need the actual token, but I do need the identity. I tried to retrieve the token as a way to test whether it was passed or not. Regardless, my setup always gives an httpcontext with null identities. I will test your setup tomorrow at work. But decoding the jwt gives "null" audiences. What should authority be?
jacquesy
jacquesy6mo ago
Authority should be your Kinde domain. Eg "https://LIFE.kinde.com"
Oli - Kinde
Oli - Kinde6mo ago
Hi @LIFE,
token to authenticate and authorize the user in the back-end
Can I confirm that you when you refer to "back-end" here, your backend is .NET?
LIFE
LIFE6mo ago
Yes, that is correct
Oli - Kinde
Oli - Kinde6mo ago
Hey @LIFE, It seems like you're trying to authenticate and authorize a user in a .NET backend using a token from a React frontend. In your .NET backend, you're using JWT Bearer authentication which is correct. However, it seems like the access token is not being sent correctly from the frontend or not being retrieved correctly in the backend. In your React frontend, you should be sending the access token in the Authorization header of your HTTP requests. Here's an example of how you can do this:
const {getToken} = useKindeAuth();

const fetchData = async () => {
try {
const accessToken = await getToken();
const res = await fetch(`<your-api>`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
const {data} = await res.json();
console.log({data});
} catch (err) {
console.log(err);
}
};
const {getToken} = useKindeAuth();

const fetchData = async () => {
try {
const accessToken = await getToken();
const res = await fetch(`<your-api>`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
const {data} = await res.json();
console.log({data});
} catch (err) {
console.log(err);
}
};
In your .NET backend, you should be able to retrieve the access token from the Authorization header of the incoming HTTP request. Here's an example of how you can do this:
[HttpGet]
public async Task<IActionResult> C()
{
var accessToken = HttpContext.Request.Headers["Authorization"].ToString().Split(' ')[1];
return Ok("test B");
}
[HttpGet]
public async Task<IActionResult> C()
{
var accessToken = HttpContext.Request.Headers["Authorization"].ToString().Split(' ')[1];
return Ok("test B");
}
Please ensure that the access token is being sent correctly from the frontend and that you're retrieving it correctly in the backend. If you're still having issues, please let me know! I'm here to help.
LIFE
LIFE6mo ago
Thanks! using the HttpContext.Request.Headers["Authorization"].ToString().Split(' ')[1]; i was able to retrieve the access token. However, the HttpContext.user is still defaulting to null:
No description
LIFE
LIFE6mo ago
The token should be sent correctly as seen here
No description
Oli - Kinde
Oli - Kinde6mo ago
Hey @LIFE, It seems like the user is not being authenticated correctly. The HttpContext.User property will be populated after the user has been authenticated. Similar to @jacquesy above, in your Startup.cs file, make sure you have the following lines in the ConfigureServices method:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
And in the Configure method, make sure you have:
app.UseAuthentication();
app.UseAuthorization();
app.UseAuthentication();
app.UseAuthorization();
These lines of code will ensure that the user is authenticated before any controller actions are executed. If HttpContext.User is still null after this, it means that the token is not valid or the user does not exist. If you're still having issues, please let me know! I'm here to help.
LIFE
LIFE6mo ago
Thank's this is all very helpful! I am still uncertain what values to put for Jwt:Issuer and Jwt:Key
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "https://kinde.com/",
ValidAudience = "",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("test"))
};
});
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "https://kinde.com/",
ValidAudience = "",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("test"))
};
});
The settings above still gives HttpContext.User = null
Oli - Kinde
Oli - Kinde6mo ago
Hi @LIFE, I think I need to ask my .NET expert team mate on this issue. Ill get back to you soon.
LIFE
LIFE6mo ago
Thank you, looking forward to the response. Appreciate the effort!
Oli - Kinde
Oli - Kinde6mo ago
In the meantime, we did just update the Kinde .NET SDK. I would suggest updating to the latest .NET SDK version.
LIFE
LIFE6mo ago
I don't use the Kinde .NET SDK as i am using the React SDK for authentication the .NET back-end is just an extension of the front-end authentication scheme
Oli - Kinde
Oli - Kinde6mo ago
Oh I understand. Thanks for explaining this. I will pass this information onto my teammates to look into.
LIFE
LIFE6mo ago
No description
Oli - Kinde
Oli - Kinde6mo ago
Ah makes more sense, thanks for the diagram.
leo_kinde
leo_kinde6mo ago
Hi @LIFE , the issuer for the JWT will be just your Kinde sub-domain, so something like https://yourbusiness.kinde.com, the public keys can be found from the JWKS endpoint on your sub-domain, something like https://yourbusiness.kinde.com/.well-known/jwks Note, Kinde supports OpenID Connect Discovery, a minimal configuration can be done with:
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://mybusiness.kinde.com";
options.Audience = "myapi";
});
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://mybusiness.kinde.com";
options.Audience = "myapi";
});
Using this .NET will look up the configuration (e.g. https://yourbusiness.kinde.com/.well-known/openid-configuration) and retrieve the keys from the JWKS endpoint to verify the token.
LIFE
LIFE6mo ago
Great, thanks! I tested 2 program.cs configs:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://alkolas.kinde.com";
options.Audience = "alkolas";
});
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://alkolas.kinde.com";
options.Audience = "alkolas";
});
var jwtKey = builder.Configuration.GetSection("Jwt:Key").Get<string>();
var jwtIssuer = builder.Configuration.GetSection("Jwt:Issuer").Get<string>();

builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
//options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.Authority = @"https://alkolas.kinde.com";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtIssuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
});
var jwtKey = builder.Configuration.GetSection("Jwt:Key").Get<string>();
var jwtIssuer = builder.Configuration.GetSection("Jwt:Issuer").Get<string>();

builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
//options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.Authority = @"https://alkolas.kinde.com";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtIssuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
});
appsettings:
"Jwt": {
"Key": "uLM_Ff-VM5i-tV8SLoMjwDMVxuIXyauSzCWQgPmu3pyqg3CaAs8M1slaylscK8vEAGhC4FwFa0zZl-SMaYmDhE-dyoxA8tvR-1WcZ6fMoerr71hJGcsNRqKbXxADfR4ELZleAFSIpyVsTpu04g2SqHLfkRSBulPoHwValwLFnYkAYhOv_o421R5SshNMEoW4zHe_z42ebXcYTh40bU_C4xna1oVFcFWi3tZWKJdcFAnUk87g5m-2LyibGcLhU5rD7IBEMjmMiF6sJUawWSvVA8szC2tcV8JrNZ-pvP3IXH1BQsXT6a_1GbzCLTNQeU2I3Hue2dhzPizreqoAhCajEQ",
"Issuer": "https://alkolas.kinde.com"
},
"Jwt": {
"Key": "uLM_Ff-VM5i-tV8SLoMjwDMVxuIXyauSzCWQgPmu3pyqg3CaAs8M1slaylscK8vEAGhC4FwFa0zZl-SMaYmDhE-dyoxA8tvR-1WcZ6fMoerr71hJGcsNRqKbXxADfR4ELZleAFSIpyVsTpu04g2SqHLfkRSBulPoHwValwLFnYkAYhOv_o421R5SshNMEoW4zHe_z42ebXcYTh40bU_C4xna1oVFcFWi3tZWKJdcFAnUk87g5m-2LyibGcLhU5rD7IBEMjmMiF6sJUawWSvVA8szC2tcV8JrNZ-pvP3IXH1BQsXT6a_1GbzCLTNQeU2I3Hue2dhzPizreqoAhCajEQ",
"Issuer": "https://alkolas.kinde.com"
},
"Jwt": {
"Key": "AQAB",
"Issuer": "https://alkolas.kinde.com"
},
"Jwt": {
"Key": "AQAB",
"Issuer": "https://alkolas.kinde.com"
},
Regardless, HttpContext.User is always null https://alkolas.kinde.com/.well-known/jwks gives:
{"keys": [{"e": "AQAB", "n": "uLM_Ff-VM5i-tV8SLoMjwDMVxuIXyauSzCWQgPmu3pyqg3CaAs8M1slaylscK8vEAGhC4FwFa0zZl-SMaYmDhE-dyoxA8tvR-1WcZ6fMoerr71hJGcsNRqKbXxADfR4ELZleAFSIpyVsTpu04g2SqHLfkRSBulPoHwValwLFnYkAYhOv_o421R5SshNMEoW4zHe_z42ebXcYTh40bU_C4xna1oVFcFWi3tZWKJdcFAnUk87g5m-2LyibGcLhU5rD7IBEMjmMiF6sJUawWSvVA8szC2tcV8JrNZ-pvP3IXH1BQsXT6a_1GbzCLTNQeU2I3Hue2dhzPizreqoAhCajEQ", "alg": "RS256", "kid": "89:b0:70:1a:e6:b9:bf:57:3b:e4:9d:a4:52:dc:f9:ea", "kty": "RSA", "use": "sig"}]}
{"keys": [{"e": "AQAB", "n": "uLM_Ff-VM5i-tV8SLoMjwDMVxuIXyauSzCWQgPmu3pyqg3CaAs8M1slaylscK8vEAGhC4FwFa0zZl-SMaYmDhE-dyoxA8tvR-1WcZ6fMoerr71hJGcsNRqKbXxADfR4ELZleAFSIpyVsTpu04g2SqHLfkRSBulPoHwValwLFnYkAYhOv_o421R5SshNMEoW4zHe_z42ebXcYTh40bU_C4xna1oVFcFWi3tZWKJdcFAnUk87g5m-2LyibGcLhU5rD7IBEMjmMiF6sJUawWSvVA8szC2tcV8JrNZ-pvP3IXH1BQsXT6a_1GbzCLTNQeU2I3Hue2dhzPizreqoAhCajEQ", "alg": "RS256", "kid": "89:b0:70:1a:e6:b9:bf:57:3b:e4:9d:a4:52:dc:f9:ea", "kty": "RSA", "use": "sig"}]}
leo_kinde
leo_kinde6mo ago
@LIFE for your requests can you have a look at the response headers? There should be a header that tells you why authentication failed when developing locally. If not you can enable this by adding options.IncludeErrorDetails = true;
LIFE
LIFE6mo ago
I sent it in an earlier message aswell, but here it is: @leo_kinde
No description
LIFE
LIFE6mo ago
:authority:
localhost:7293
:method:
GET
:path:
/Test/B
:scheme:
https
Accept:
application/json
Accept-Encoding:
gzip, deflate, br
Accept-Language:
nb-NO,nb;q=0.9,no;q=0.8,en-US;q=0.7,en;q=0.6,ar;q=0.5
Authorization:
Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg5OmIwOjcwOjFhOmU2OmI5OmJmOjU3OjNiOmU0OjlkOmE0OjUyOmRjOmY5OmVhIiwidHlwIjoiSldUIn0.eyJhdWQiOltdLCJhenAiOiIzYTRmNjdmNWQwZDI0OGVmODQyMjE0Y2NhY2RiNTIxMCIsImJpbGxpbmciOnsiaGFzX3BheW1lbnRfZGV0YWlscyI6ZmFsc2UsInBsYW4iOnt9fSwiZW1haWwiOiJjdG9AZmFydHNrcml2ZXIubm8iLCJleHAiOjE3MDIwMzM2NDksImlhdCI6MTcwMTk0NzI0OSwiaXNzIjoiaHR0cHM6Ly9hbGtvbGFzLmtpbmRlLmNvbSIsImp0aSI6ImFkYmRmZjU4LWJiMzctNGVmOS1iN2U2LTZlODc5YjVjNzE1MiIsIm9yZ19jb2RlIjoib3JnX2E3MTI0YjExMmUzIiwib3JnX25hbWUiOiJEZWZhdWx0IE9yZ2FuaXphdGlvbiIsInBlcm1pc3Npb25zIjpbImFkbWluIl0sInJvbGVzIjpbeyJpZCI6IjAxOGMzNDIyLTg2MjUtZTBjOS1mYTM2LWVjODQ0Y2I5NmRjOCIsImtleSI6ImFkbWluIiwibmFtZSI6ImFkbWluIn1dLCJzY3AiOlsib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIiwib2ZmbGluZSJdLCJzdWIiOiJrcF9iNTVlN2YwYmZmMGU0YjMxYmU2MGFmZDkyMmM3MzhiMCJ9.mbLRszIgimhkuxsLPKcyVJQY87ebDCU1U-AWyU1mB91oXlW7C-IJcVY0p1oOrzcN8iKAZVYSuTXC9j2YctHmU0wLAJ9N-OYLXWlMU-24YzMc3SVcERZkHqWVX99UKUu1hN1MxB3Z9_QjPT_PCthX7xE3JLG0RUL0xK6iI5VFh554ZsOX7gcmzIVAD2wnkVeY9civo1bOuElWS1AnE2iRVlhUUrPPHIkjBAf4bYfMihTOEtioTUTb82LpsXWdaCidoU4gY3ADSOWg99zKnXAfTxycjUvQG6mnA5kLCXSJBScoQoPn7CIqXM3t1-9ZaYuSJDXGp7_I5-Ah3OSycEFytQ
Origin:
https://localhost:5173
Referer:
https://localhost:5173/
Sec-Ch-Ua:
"Google Chrome";v="119", "Chromium";v="119", "Not?A_Brand";v="24"
Sec-Ch-Ua-Mobile:
?0
Sec-Ch-Ua-Platform:
"Windows"
Sec-Fetch-Dest:
empty
Sec-Fetch-Mode:
cors
Sec-Fetch-Site:
same-site
User-Agent:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
:authority:
localhost:7293
:method:
GET
:path:
/Test/B
:scheme:
https
Accept:
application/json
Accept-Encoding:
gzip, deflate, br
Accept-Language:
nb-NO,nb;q=0.9,no;q=0.8,en-US;q=0.7,en;q=0.6,ar;q=0.5
Authorization:
Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg5OmIwOjcwOjFhOmU2OmI5OmJmOjU3OjNiOmU0OjlkOmE0OjUyOmRjOmY5OmVhIiwidHlwIjoiSldUIn0.eyJhdWQiOltdLCJhenAiOiIzYTRmNjdmNWQwZDI0OGVmODQyMjE0Y2NhY2RiNTIxMCIsImJpbGxpbmciOnsiaGFzX3BheW1lbnRfZGV0YWlscyI6ZmFsc2UsInBsYW4iOnt9fSwiZW1haWwiOiJjdG9AZmFydHNrcml2ZXIubm8iLCJleHAiOjE3MDIwMzM2NDksImlhdCI6MTcwMTk0NzI0OSwiaXNzIjoiaHR0cHM6Ly9hbGtvbGFzLmtpbmRlLmNvbSIsImp0aSI6ImFkYmRmZjU4LWJiMzctNGVmOS1iN2U2LTZlODc5YjVjNzE1MiIsIm9yZ19jb2RlIjoib3JnX2E3MTI0YjExMmUzIiwib3JnX25hbWUiOiJEZWZhdWx0IE9yZ2FuaXphdGlvbiIsInBlcm1pc3Npb25zIjpbImFkbWluIl0sInJvbGVzIjpbeyJpZCI6IjAxOGMzNDIyLTg2MjUtZTBjOS1mYTM2LWVjODQ0Y2I5NmRjOCIsImtleSI6ImFkbWluIiwibmFtZSI6ImFkbWluIn1dLCJzY3AiOlsib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIiwib2ZmbGluZSJdLCJzdWIiOiJrcF9iNTVlN2YwYmZmMGU0YjMxYmU2MGFmZDkyMmM3MzhiMCJ9.mbLRszIgimhkuxsLPKcyVJQY87ebDCU1U-AWyU1mB91oXlW7C-IJcVY0p1oOrzcN8iKAZVYSuTXC9j2YctHmU0wLAJ9N-OYLXWlMU-24YzMc3SVcERZkHqWVX99UKUu1hN1MxB3Z9_QjPT_PCthX7xE3JLG0RUL0xK6iI5VFh554ZsOX7gcmzIVAD2wnkVeY9civo1bOuElWS1AnE2iRVlhUUrPPHIkjBAf4bYfMihTOEtioTUTb82LpsXWdaCidoU4gY3ADSOWg99zKnXAfTxycjUvQG6mnA5kLCXSJBScoQoPn7CIqXM3t1-9ZaYuSJDXGp7_I5-Ah3OSycEFytQ
Origin:
https://localhost:5173
Referer:
https://localhost:5173/
Sec-Ch-Ua:
"Google Chrome";v="119", "Chromium";v="119", "Not?A_Brand";v="24"
Sec-Ch-Ua-Mobile:
?0
Sec-Ch-Ua-Platform:
"Windows"
Sec-Fetch-Dest:
empty
Sec-Fetch-Mode:
cors
Sec-Fetch-Site:
same-site
User-Agent:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
LIFE
LIFE6mo ago
No description
LIFE
LIFE6mo ago
Seems like audience is empty in the token
LIFE
LIFE6mo ago
yeah, audience is empty:
No description
LIFE
LIFE6mo ago
Thought that was handled by kinde?
leo_kinde
leo_kinde6mo ago
By default we don't set an audience, just .NET seems to require one. You can configure one in the Kinde admin UI. If you go to Settings then APIs under Environment. Then enable that API on the Application under Applications. When you initiate auth you'll need specify the audience too. In the React SDK I think this is configured as a prop on the provider.
LIFE
LIFE6mo ago
Now it works. Thank you, really appreciate the good help!
No description
No description
No description
leo_kinde
leo_kinde6mo ago
No worries @LIFE , glad it is working. If you happen to want the Name property to map to the Kinde user id, you can add:
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "sub";
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "sub";
LIFE
LIFE6mo ago
That's great! Really appreciate the extra tip there! Now i get both name & email (in claims) Btw: Do you have any documentation for this specific setup?
No description
leo_kinde
leo_kinde6mo ago
Unfortunately we don't have a document public yet @LIFE , but it is something we're working on. Including initiating .NET with Kinde auth on backend without using the SDK as that can also be done with built-in .NET auth and configuration.
Unknown User
Unknown User6mo ago
Message Not Public
Sign In & Join Server To View
LIFE
LIFE6mo ago
would i have to make changes to my authorization setup in program.cs to allow for the [Authroize(Roles = "admin")] attribute to work? Would i have to go about it this way: https://stackoverflow.com/a/72472743/3712531
No description
No description
LIFE
LIFE6mo ago
was able to "solve" it with this, but i'm uncertain if there are better solutions to the problem: GitHub Copilot: To authorize based on the "name" value in the "roles" claim, you would need to parse the claim value as JSON and check the "name" field. However, the built-in RequireClaim method doesn't support this kind of complex claim value checking. You will need to create a custom IAuthorizationRequirement and AuthorizationHandler to handle this. Here's an example of how you can do it:
public class RolesRequirement : IAuthorizationRequirement
{
public string RoleName { get; }

public RolesRequirement(string roleName)
{
RoleName = roleName;
}
}

public class RolesHandler : AuthorizationHandler<RolesRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesRequirement requirement)
{
var rolesClaim = context.User.Claims.FirstOrDefault(c => c.Type == "roles");
if (rolesClaim != null)
{
var roles = JsonSerializer.Deserialize<Dictionary<string, string>>(rolesClaim.Value);
if (roles != null && roles.TryGetValue("name", out var roleName) && roleName == requirement.RoleName)
{
context.Succeed(requirement);
}
}

return Task.CompletedTask;
}
}
public class RolesRequirement : IAuthorizationRequirement
{
public string RoleName { get; }

public RolesRequirement(string roleName)
{
RoleName = roleName;
}
}

public class RolesHandler : AuthorizationHandler<RolesRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesRequirement requirement)
{
var rolesClaim = context.User.Claims.FirstOrDefault(c => c.Type == "roles");
if (rolesClaim != null)
{
var roles = JsonSerializer.Deserialize<Dictionary<string, string>>(rolesClaim.Value);
if (roles != null && roles.TryGetValue("name", out var roleName) && roleName == requirement.RoleName)
{
context.Succeed(requirement);
}
}

return Task.CompletedTask;
}
}
Then, you can add the requirement and handler to your services and use them in your policy:
builder.Services.AddSingleton<IAuthorizationHandler, RolesHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("admin", policy => policy.Requirements.Add(new RolesRequirement("admin")));
});
builder.Services.AddSingleton<IAuthorizationHandler, RolesHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("admin", policy => policy.Requirements.Add(new RolesRequirement("admin")));
});
This will create a policy named "admin" that requires the "roles" claim to have a "name" field with the value "admin".
leo_kinde
leo_kinde6mo ago
Thanks for sharing this @LIFE. I'm not super familiar with this part of handling in .NET, but it sounds reasonable.
jacquesy
jacquesy6mo ago
@LIFE - This is pretty much exactly what I've done in my solution. You can now protect the whole controller or a specific action method using the [Authorize(Policy = "admin")] attribute.
Want results from more Discord servers?
Add your server
More Posts