Azure AD B2C keeps providing v1 tokens not v2 tokens - jwt

Azure's AD B2C keeps issuing v1 tokens even though v2 tokens are configured in the manifest of the SPA app that's registered:
{
"id": "XXX",
"acceptMappedClaims": null,
"accessTokenAcceptedVersion": 2,
"addIns": [],
"allowPublicClient": null,
...
}
The client uses #azure/msal-angular v2.0.5 (along with #azure/msal-browser v2.19.0) to request the token via a plain MSAL Interceptor:
export const protectedResourceMap: Map<string, Array<string>> = new Map([
[
urlJoin(configs.apiUri, 'screen'),
[configs.authConfig.scope.screen_access],
],
]);
#NgModule({
imports: [
MsalModule.forRoot(
new PublicClientApplication({
auth: {
clientId: '...',
authority: 'https://login.microsoftonline.com/XXX.onmicrosoft.com',
postLogoutRedirectUri: '.../logout',
navigateToLoginRequestUrl: true,
redirectUri: '.../auth',
},
cache: {
cacheLocation: 'sessionStorage',
},
}),
{
interactionType: InteractionType.Redirect, // Popup or Redirect
loginFailedRoute: '/login-failed'
},
{
interactionType: InteractionType.Redirect, // Popup or Redirect
protectedResourceMap,
})
...
This seems to look OK, especially the "accessTokenAcceptedVersion": 2.
What might be the root cause of the token still being of v1?
{
"aud": "00000003-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"iss": "https://sts.windows.net/7dcXX-XXXXX.../",
...
"ver": "1.0",
...
}
Pointers would be much appreciated.

Azure AD B2C only ever used the endpoint when making the OIDC Authentication requests with v2.0, a v1.0 never existed. So it always has issued v1.0 tokens (v2 is the first and only version). This is completely normal.
Only Azure AD had v1.0 and v2.0 OIDC endpoint, and therefore maps based off of accessTokenAcceptedVersion.
You don't need to mess with this property in AAD B2C application registrations unless you have a SAML relying party.

Related

ASP.NET 6: Azure AD Authentication Infinite redirect loops with AWS Network LB and Fargate

I have a AWS Network Load balancer setup with a TLS (:443) Listener that forwards to a Target Group that is listening on port 8080.
The Target Group is an IP Type that points to a Fargate ECS instance.
My problem is that on that ECS instance my website is using Azure Ad for Auth. I got past the issue of the Redirect URI being HTTP instead of HTTPS, but now I am in a redirect loop that eventually ends in
We couldn't sign you in. Please try again.
I am using .NET 6 and Visual Studio 2022.
The Azure AD Auth was added via using the Connected Services in VS 2022.
The NLB URL has been added to Redirect URIs for the App in Azure AD.
Any help is appreciated.
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "xxxxxxxxx.com",
"TenantId": "xxxxxxxxxx",
"ClientId": "xxxxxxxxxx",
"CallbackPath": "/signin-oidc"
},
"MicrosoftGraph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "user.read"
}
}
program.cs
var builder = WebApplication.CreateBuilder(args);
var initialScopes = builder.Configuration["MicrosoftGraph:Scopes"]?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
.AddInMemoryTokenCaches();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
// Add services to the container.
builder.Services.AddRazorPages().AddMicrosoftIdentityUI();
builder.Services.AddScoped<IDynamoDBConnection, DynamoDBConnection>();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
builder.WebHost.UseUrls("http://*:8080");
var app = builder.Build();
//This is what fixes the Http redirect URI issue. Problem is it causes a redirect loop
app.Use((context, next) =>
{
context.Request.Scheme = "https";
return next(); //return next(context); //rewritten 8/19 8:23 no change
});
app.UseForwardedHeaders();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
I have tried multiple browsers and the issue is the same.
I ran into this same issue and managed to resolve by adding the client secret to the appsettings.json.
In the Azure portal, go to Active Directory -> App registrations -> your-app -> Certificates & secrets. Add a new client secret, copy the Value (not the Secret ID, I gave myself an extra headache making that mistake) and paste it into your appsettings Azure object like so:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "xxxxxxxxx.com",
"TenantId": "xxxxxxxxxx",
"ClientId": "xxxxxxxxxx",
"CallbackPath": "/signin-oidc",
"ClientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

Keycloak Step Up from Client

The Keycloak documentation here says you need to add ACR with claims in the request in order to do authentication step up to a higher level. But how is this accomplished from either the keycloak.js client library, or the keycloak-js npm client library?
So basically, how do you get the following claims query param to be passed?
https://{DOMAIN}/realms/{REALMNAME}/protocol/openid-connect/auth?client_id={CLIENT-ID}&redirect_uri={REDIRECT-URI}&scope=openid&response_type=code&response_mode=query&nonce=exg16fxdjcu&claims=%7B%22id_token%22%3A%7B%22acr%22%3A%7B%22essential%22%3Atrue%2C%22values%22%3A%5B%22gold%22%5D%7D%7D%7D
The format of the claims is like this as seen in the documentation:
claims= {
"id_token": {
"acr": {
"essential": true,
"values": ["gold"]
}
}
}
Doing this off the top of my head, but I think this should do it.
const keycloak = Keycloak({
url: {DOMAIN},
realm: {REALMNAME},
clientId: {CLIENT-ID}
});
keycloak.login({
... your login options
acr: { values: ["silver", "gold"], essential: true }
})
The adapter will take the acr option and apply it to claims.id_token

clientId missing from resource_access field in jwt token when using impersonation

I'm using Keycloak 14.0.0 and enabled the feature preview of token_exchange in order to do impersonation. After configuring my user in Keycloak to take on the impersonation role on the client "realm-management" (as according to the [documentation][1]), the actual request to do the token exchange fails as the token is not valid.
After some debugging it turns out that the jwt token is indeed malformed:
...
"session_state": "a03aeg0e-b5ce-4a50-9038-c339e50338c4",
"acr": "1",
"allowed-origins": [
"http://0.0.0.0:9180"
],
"scope": "openid identity_provider email admin profile company",
"permissions": [
"consented-readonly",
"readonly",
"trackingdisabled"
],
"resource_access": {
".roles": [
"impersonation"
]
},
"email_verified": false,
"idp": "myidp",
...
In the above, please notice the ".roles". I assume this is incorrect. It should be something like:
"resource_access": {
"myclient": {
"roles": [
"impersonation"
]
}
How can this be fixed?
[1]: https://www.keycloak.org/docs/latest/securing_apps/index.html#impersonation
It turns out that the configuration of a mapper was incorrect. In this case it was the "client roles" mapper (client scopes -> roles -> mapper -> client roles in keycloak ui) which, in my keycloak setup, had the value of:
resource_access..roles
This is incorrect as it should contain a clientId placeholder as shown below:
resource_access.${client_id}.roles
after this change the accessToken includes the actual client resulting in a valid json in the accessToken

Cloud Run Service requests always fail with 401 when using JWT returned for another service. The same request works with JWT returned for end-user

I have an app consisting of multiple Cloud Run Services. Each service is open only to a specific set of other services.
Let's say I have 2 services with URLs https://api-service-123.run.app and https://data-provider-service-123.run.app. The first service also has a custom URL https://api.my.domain.
api-service needs access to data-provider-service. So, following the documentation, I created two per-service user-managed service-accounts api-service-account#domain and data-provider-service-account#domain. I added api-service-account#domain into data-provider-service with Cloud Run Invoker role.
Now, I have trouble accessing the account with ID Token returned from Google. I tried several ways. Firstly, I utilized slightly adjusted code from the docks
export const getToken = async (url: string, targetAUD?: string) => {
const auth = new GoogleAuth();
const request = async () => {
if (!targetAUD) {
targetAUD = new URL(url).origin;
}
console.info(`request ${url} with target audience ${targetAUD}`);
const client = await auth.getIdTokenClient(targetAUD);
const res = await client.request({url});
console.info(res.data);
return res.data;
}
try {
return await request();
} catch (err) {
console.error(err);
}
}
When the function above is called as getToken('http://data-provider-service-123.run.app');, the request fails with 401 Unauthorized.
I also tried to call it as getToken('https://data-provider-service-123.run.app'); and got a response 403 Forbidden. The following entry is added to data-provider-service logs for each such request
{
httpRequest: {9}insertId: "60fcb7e0000c12346fe37579"
logName: "projects/my-project/logs/run.googleapis.com%2Frequests"
receiveTimestamp: "2021-07-25T01:01:20.806589957Z"
resource: {2}
severity: "WARNING"
textPayload: "The request was not authorized to invoke this service. Read more at
https://cloud.google.com/run/docs/securing/authenticating"
timestamp: "2021-07-25T01:01:20.800918Z"
trace: "projects/my-project/traces/25b3c13890aad01234829711d4e9f25"
}
I also tried to obtain and use JWT from compute metadata server (also a way advertised in the docs) by running curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=http://data-provider-servive-123.run.app" \ -H "Metadata-Flavor: Google".
When I execute this code in Cloud Shell, I get JWT that looks like this
{
"iss": "accounts.google.com",
"azp": "123456789-9r9s1c4alg36erliucho9t52n12n6abc.apps.googleusercontent.com",
"aud": "123456789-9r9s1c4alg36erliucho9t52n12n6abc.apps.googleusercontent.com",
"sub": "106907196154567890477",
"email": "my_email#gmail.com",
"email_verified": true,
"at_hash": "dQpctkQE2Sy1as1qv_w",
"iat": 1627173901,
"exp": 1627177501,
"jti": "448d660269d4c7816ae3zd45wrt89sb9f166452dce"
}
Then I make a request using postman to https://data-provider-service/get-data with a header Authorization: "Bearer <the_jwt_from_above>" and everything works fine
However, if I make the same request from my api-service, the JWT returned is
{
"aud": "http://data-provider-service-123.run.app",
"azp": "111866132465987679716",
"email": "api-service-account#domain",
"email_verified": true,
"exp": 1627179383,
"iat": 1627175783,
"iss": "https://accounts.google.com",
"sub": "111866132465987679716"
}
If I make the same request with the postman and place this JWT into the Authorization header, the 401 Unauthorized is returned.
I have spent this week trying to solve this issue. I triple-checked the permissions, redeployed the services several times, but nothing helps.
I changed http to https in the URL of the target service when asking computing metadata server, and looks like it solved the problem. Requesting token like this with google-auth-library still fails with 403 error.
I'll update that answer if the solution is sustainable.

JWT token miss the claim in Nuxt Auth module with Auth0

In my Nuxt app after successful log in I have claim from Auth0. Devtools console shows it:
$vm0.$auth.$state.user['https://hasura.io/jwt/claims']
{x-hasura-default-role: "user", x-hasura-allowed-roles: Array(1), x-hasura-user-id: "auth0|5e989a*******"}
But my token doesn't have this claim, so my hasura requests fails. I check token right after claim in console:
$vm0.$auth.getToken('auth0')
"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik1ESTNNa0l5...
And I decode it using https://jwt.io/ and there is no claim:
{
"iss": "https://*******.eu.auth0.com/",
"sub": "auth0|5e989a*******",
"aud": [
"https://*******.eu.auth0.com/api/v2/",
"https://*******.eu.auth0.com/userinfo"
],
"iat": 1587059738,
"exp": 1587066938,
"azp": "MxJB0Y9dxvdGZnTAb2a4Y0YOHArvbkWt",
"scope": "openid profile email"
}
Here is my claim script in Auth0 dashboard:
function (user, context, callback) {
const namespace = "https://hasura.io/jwt/claims";
context.idToken[namespace] =
{
'x-hasura-default-role': 'user',
// do some custom logic to decide allowed roles
'x-hasura-allowed-roles': ['user'],
'x-hasura-user-id': user.user_id
};
callback(null, user, context);
}
I fixed this issue by changing claim script in Auth0 dashboard.
It should be context.accessToken[namespace] = {...} instead of context.idToken[namespace] = {...} and now I have that claim data inside my token.