I am creating an identity provider, and have plumbed it into a B2C custom policy.
Discovery happens fine, and the user gets sent to my idp fine.
However, when I return the user back to B2C, using the id_token response type (the only response type I am supporting at the moment) B2C gives me the error in the title. The full error is:
error_description=AADB2C90239: The provided id_token failed signature validation. Please provide another token and try again.
Everything else I am validating my token against (jwt.io, various node jwt/jwk verification methods) seems to be happy with what I am returning, only B2C isn't.
Here is an example of my id_token response
I am just running it on my local machine, hence the issuer being ngrok, but my openid-configuration looks like this:
{
"issuer": "https://28b5fe46.ngrok.io",
"authorization_endpoint": "http://localhost:3000/authentication/auth",
"jwks_uri": "https://28b5fe46.ngrok.io/.well-known/openid-configuration/keys",
"response_modes_supported": [
"query"
],
"response_types_supported": [
"id_token"
],
"scopes_supported": [
"openid"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"claims_supported": [
"sub"
]
}
and my keys look like this:
{
"keys": [
{
"kid": "kiddy123",
"alg": "RS256",
"kty": "RSA",
"use": "sig",
"n": "ALdnY_dWrQgjaqWuqUFpr-__p62xsWSWGiVH9CEWlxOTGrR8jhfG_C_xxGkkptWrVWwJgpmJ7zOMFXjqc9HqGCitl9Czl-X68Ld2xnZ_HdmikRTv-Witn8V-5QiQhSEGwvA2Xek8OVWKWOZ7Z4L9_SjNar33E8zGGFtz77_pmzJ6_zompGQSkLwAMTU3buI8TUKFIOBLGtc_eFCfgcbHFjJuYYp_200A4Xz2a0lNvvHSZ8PLKmwoRIBbFhkhe0zFd0mwq97SavTUXrltZnq3ghTbc6QzJC4T3_4J1LMTEQIFpaK3jK9VPFrLVESy8Ovz4CdxsfY3_bv2QX2HfPOxJziDIOsoztXUvfPPH-tu5nZ2JpKuE5ftAL3W7LbR5SCe2fHqV8aSsJbyDP2fHKbjy1pBhx_MQ2ty17YJRcqHER0l2GEhRf7AhdewIdv2_LdMIUr1tYuYWLuiZVN682fgYZZGl_TAxhBSyi65uzXbziG4STpENkX8KvBPjkMJc-EfwRcegJzS2kJVd-fnE5fCF2lHuo0hC93piViUhtzo6_1R5AXKk2JKxs_kWRd30E7DE8LZPRw-2hM3zrEQ6X5VL7q-UvHLR6SUKdjHXPYUX2FJAuj8EhQlqhovf26_pwO26wHhBl8mkJo9T8c8MQSVz3y12AJbP99-lo0We5umk4uP",
"e": "AQAB"
}
]
}
The B2C journey recorder doesn't give me any extra information, only the aforementioned error.
Does anyone know how I might debug this further?
Here is how I am creating the id_token. You can see near the bottom I am validating the token against the pem that was used to create the jwks.
const { session } = common.getServices(request)
const sessionValues = session.get(request)
const pem = fs.readFileSync(path.join(__dirname, '..', '..', '..', '..', '..', 'jwtRS256.key'))
const {
nonce,
redirectUri,
state,
databucketItems: { sub },
clientId
} = sessionValues
const jwk = njwk.JWK.fromJSON(JSON.stringify({
'kid': 'kiddy123',
'alg': 'RS256',
'kty': 'RSA',
'use': 'sig',
'n': 'ALdnY_dWrQgjaqWuqUFpr-__p62xsWSWGiVH9CEWlxOTGrR8jhfG_C_xxGkkptWrVWwJgpmJ7zOMFXjqc9HqGCitl9Czl-X68Ld2xnZ_HdmikRTv-Witn8V-5QiQhSEGwvA2Xek8OVWKWOZ7Z4L9_SjNar33E8zGGFtz77_pmzJ6_zompGQSkLwAMTU3buI8TUKFIOBLGtc_eFCfgcbHFjJuYYp_200A4Xz2a0lNvvHSZ8PLKmwoRIBbFhkhe0zFd0mwq97SavTUXrltZnq3ghTbc6QzJC4T3_4J1LMTEQIFpaK3jK9VPFrLVESy8Ovz4CdxsfY3_bv2QX2HfPOxJziDIOsoztXUvfPPH-tu5nZ2JpKuE5ftAL3W7LbR5SCe2fHqV8aSsJbyDP2fHKbjy1pBhx_MQ2ty17YJRcqHER0l2GEhRf7AhdewIdv2_LdMIUr1tYuYWLuiZVN682fgYZZGl_TAxhBSyi65uzXbziG4STpENkX8KvBPjkMJc-EfwRcegJzS2kJVd-fnE5fCF2lHuo0hC93piViUhtzo6_1R5AXKk2JKxs_kWRd30E7DE8LZPRw-2hM3zrEQ6X5VL7q-UvHLR6SUKdjHXPYUX2FJAuj8EhQlqhovf26_pwO26wHhBl8mkJo9T8c8MQSVz3y12AJbP99-lo0We5umk4uP',
'e': 'AQAB'
}))
const time = new Date().getTime()
const jwt = njwt.create({
iss: 'https://28b5fe46.ngrok.io',
name: 'Cheese man',
aud: clientId,
nonce,
redirectUri,
state,
sub
}, pem, jwk.alg)
jwt.setHeader('kid', 'kiddy123')
jwt.setExpiration(time + (12 * 60 * 60 * 1000))
jwt.setNotBefore(time - (2 * 60 * 60 * 1000))
const compacted = jwt.compact()
const returnUrl = `${sessionValues.redirectUri}?${qs.stringify({
id_token: compacted,
state
})}`
/** Just for verifying the id_token**/
const verifier = njwt.createVerifier().withKeyResolver((kid, next) => {
return pem.toString()
})
const parsedJwt = verifier.verify(compacted)
console.log(parsedJwt) // This prints the token correctly - meaning it is valid
return h.redirect(returnUrl)
Related
I was going to create a private-scoped endpoint on my Express.js backend API to check some custom permissions. I'm using RBAC (Role-Based Access Control) in auth0 with the 'express-oauth2-jwt-bearer' (https://www.npmjs.com/package/express-oauth2-jwt-bearer) package. I constantly get an Insufficient Scope Error when I try to access that endpoint.
Express Code,
const express = require('express');
const app = express();
const { auth, requiredScopes } = require('express-oauth2-jwt-bearer');
const checkJwt = auth();
const requiredScopes = requiredScopes("getAll:student", { customScopeKey: "permissions" });
app.get(
"/student/getAll",
checkJwt,
requiredScopes,
res.json({
message: 'Here is the all Student detail list'
});
);
Decoded JSON web token details,
[1]: https://i.stack.imgur.com/EtZfU.jpg
{
"iss": "https://*************.us.auth0.com/",
"sub": "auth0|******************",
"aud": [
"http://*************",
"https://*************.us.auth0.com/userinfo"
],
"iat": 1657083984,
"exp": 1657170384,
"azp": "***********************",
"scope": "openid profile email",
"permissions": [
"delete:student",
"getAll:student",
"search:student",
"update:student"
]
}
But if I use const requiredScopes = requiredScopes("openid profile email", { customScopeKey: "permissions" }); instead of const requiredScopes = requiredScopes("getAll:student", { customScopeKey: "permissions" }); it works. I think the problem is permissions are not check against custom scope key but with default scope key. Anyone can help to fix it ?
I'm having the same issue - it seems that requiredScopes is broken. What I did instead was use claimCheck from the same library:
const checkClaims = claimCheck(claims => {
return claims.permissions.includes('your:permission')
});
Apologies for my previous badly formulated question.
I am trying to write a standalone NodeJS app, that retrieves activity data from the Google Fit REST API and writes it locally as a json file. The app will run unattended on a headless raspberry pi, so there are no "browser pop-up" windows in order to authenticate with a Google account.
I activated my Google Fit API and created service account credentials. My key is stored as MY_KEY.json in this example. Now using the googleapis library, I create a JWT token signed with MY_KEY.json, and authenticate with it when sending my request.
I get a response from the API, but the data is empty. (I know there is data, since doing a similar request in a browser with a traditional oauth2 flow returns all my activity data sessions. I wonder if authentication with JWT tokens using a service account is allowed for fitness data ?
Here is my code :
'use strict';
const {google, fitness_v1} = require('googleapis');
const path = require('path');
const fs = require('fs');
async function runSample() {
// Create a new JWT client using the key file downloaded from the Google Developer Console
const auth = new google.auth.GoogleAuth({
keyFile: path.join(__dirname, 'MY_KEY.json'),
scopes: 'https://www.googleapis.com/auth/fitness.activity.read',
});
const client = await auth.getClient();
// Obtain a new fitness client, making sure you pass along the auth client
const fitness_v1 = google.fitness({
version: 'v1',
auth: client
});
//console.log(client);
const res = await fitness_v1.users.sessions.list({
"userId": "me"
});
fs.writeFile('session.json', JSON.stringify(res.data, null, 4), (err) => {
if (err) {
throw err;
}
console.log("Retrieved sessions are saved as JSON.");
});
console.log(res.data);
return res.data;
}
if (module === require.main) {
runSample().catch(console.error);
}
// Exports for unit testing purposes
module.exports = {runSample};
The response is :
{
session: [],
deletedSession: [],
nextPageToken: 'XmRh96blablablan4yFjZQ'
}
It should be :
{
"session": [
{
"id": "healthkit-D7B3AC93-3739-4E04-978A-C53599D8401E",
"name": "Uni",
"description": "",
"startTimeMillis": "1645651234280",
"endTimeMillis": "1645676584280",
"modifiedTimeMillis": "1645676989684",
"application": {
"packageName": "com.apple.workout",
"version": "",
"detailsUrl": ""
},
"activityType": 72
},
{
"id": "healthkit-3691B45B-9D51-4C14-ACC6-EC9DB48B5F23",
"name": "Juoksu",
"description": "",
"startTimeMillis": "1647073527249",
"endTimeMillis": "1647075778248",
"modifiedTimeMillis": "1647076769108",
"application": {
"packageName": "runkeeperpro",
"version": "",
"detailsUrl": ""
},
"activityType": 56
}
],
"deletedSession": [],
"nextPageToken": "XmRh96blablablan4yFjZQ"
}
Any suggestions ? Thanks in advance.
According to Addendum: Service account authorization without OAuth:
With some Google APIs, you can make authorized API calls using a signed JWT directly as a bearer token, rather than an OAuth 2.0 access token. (snip)
If the API you want to call has a service definition published in the Google APIs GitHub repository, you can make authorized API calls using a JWT instead of an access token.
There is no such service definition for Fit, so, unfortunately, the answer is that you can't use JWT.
I am upgrading an app using msal.js v1.3 to v2.3 and I'm having a problem retreiving the access token once I get my id token.
I initialize the handleRedirectPromise in my constructor. Then, when the user clicks the login button, I call loginRedirect and pass in an object that has the openid scope and the scope from my separately registered api. This works well, the id token comes back and I call acquireTokenSilent to retreive my access token. I pass an object that has my registered api's scope and account from the loginRedirect call into this function.
The problem is that the authorization response from the acquireTokenSilent has an empty access token. The result from the token endpoint looks like:
client_info: "xx"
id_token: "xx"
not_before: 1602895189
refresh_token: "xx"
refresh_token_expires_in: 1209600
scope: ""
token_type: "Bearer"
It doesn't have an access token, but it does specifiy the token type as Bearer
There is no access token in the response and it looks like the scopes property returning is empty. Here is my code:
private msalConfig: Msal.Configuration = {
auth: {
clientId: environment.clientID,
authority: 'https://<tenant>.b2clogin.com/<tenant>.onmicrosoft.com/B2C_1_DefaultSignInSignUp',
knownAuthorities: ['<tenant>.b2clogin.com'],
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
}
};
private loginRequest: Msal.RedirectRequest = {
scopes: ['openid' , 'offline_access', 'https://<tenant>.onmicrosoft.com/api/read' ]
};
private accessTokenRequest: Msal.SilentRequest = {
scopes: ['https://<tenant>.onmicrosoft.com/api/read'] ,
account: null
};
constructor() {
const _this = this;
this.msalInstance = new Msal.PublicClientApplication(this.msalConfig);
this.aquireSilent = (request: Msal.SilentRequest): Promise<Msal.AuthenticationResult> => {
return _this.msalInstance.acquireTokenSilent(request).then(
access_token => {
_this.cacheExpiration(access_token.expiresOn);
_this.isLoggedIn$.next(true);
return access_token;
},
function (reason) {
console.error(reason);
},
);
};
this.msalInstance
.handleRedirectPromise()
.then((tokenResponse: Msal.AuthenticationResult) => {
if (tokenResponse !== null) {
const id_token = tokenResponse.idToken;
const currentAccounts = this.msalInstance.getAllAccounts()
this.accessTokenRequest.account = currentAccounts[0];
this.aquireSilent(this.accessTokenRequest)
}
})
.catch(error => {
console.error(error);
});
}
public login() {
this.msalInstance.loginRedirect(this.loginRequest);
}
Why is the access token not coming back from the token endpoint? Does it have to do with the scopes returning empty? I tried removing the scopes and putting in invalid entries and an error gets raised so I know my request going out is at least valid. Also, just to verify, I have 2 app registrations in AAD, one I created for my spa that has code flow and my older registration I have for my api with an exposed api and scope.
acquireTokenSilent will return an access token only if there is already an entry for that token in the cache. So if for some reason the token was never obtained previously (via loginRedirect, for instance), it will not be able to acquire it silently.
That seems to be the issue in your case. You are mixing scopes for different resources in your loginRequest, and that's perhaps causing the issue in the new version of the library (access tokens are issued per-resource-per-scope(s). See this doc for more) Try modifying your loginRequest object like this:
private loginRequest: Msal.RedirectRequest = {
scopes: ['openid', 'offline_access' ],
extraScopesToConsent:['https://<tenant>.onmicrosoft.com/api/read']
};
Also, the recommended pattern of usage with acquireTokenSilent is that you should fall back to an interactive method (e.g. acquireTokenRedirect) if the acquireTokenSilent fails for some reason.
So I would modify it as:
this.aquireSilent = (request: Msal.SilentRequest): Promise<Msal.AuthenticationResult> => {
return _this.msalInstance.acquireTokenSilent(request).then(
access_token => {
// fallback to interaction when response is null
if (access_token === null) {
return _this.msalInstance.acquireTokenRedirect(request);
}
_this.cacheExpiration(access_token.expiresOn);
_this.isLoggedIn$.next(true);
return access_token;
},
function (reason) {
if (reason instanceof msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return _this.msalInstance.acquireTokenRedirect(request);
} else {
console.warn(reason);
}
},
);
};
A similar issue is discussed here
Building a basic application where users can find Service Providers using MEAN Stack, and after negotiations are over, agreements are auto generated and have to be signed by both parties.
Got Stuck on generation of JWT Token for authentication.
Steps I followed are:
Generate a url for obtaining consent from user and pass it to frontend. Users will be redirected and permissions can be granted from there.
var url = "https://account-d.docusign.com/oauth/auth?response_type=code&scope=signature&client_id=42017946-xxxx-xxxx-xxxx-81b0ca97dc9a&redirect_uri=http://localhost:4200/authorization_code/callback";
res.status(200).json({
status: 1,
message: 'Fetched',
value: url
});
After successful redirection with code in URL, API call is made to backend for the generation of JWT token.
Token is generated as follows:
var jwt = require('jsonwebtoken');
var privateKey = fs.readFileSync(require('path').resolve(__dirname, '../../src/environments/docusign'));
const header = {
"alg": "RS256",
"typ": "JWT"
};
const payload = {
iss: '42017946-xxxx-xxxx-a5cd-xxxxxx',
sub: '123456',
iat: Math.floor(+new Date() / 1000),
aud: "account-d.docusign.com",
scope: "signature"
};
var token = jwt.sign(payload, privateKey, { algorithm: 'RS256', header: header });
Private key used above is from docusign admin panel.
iss -> Integration key against my app.
sub -> user id in the drop down of user symbol in admin panel
Obtain the access token
const axios = require('axios');
axios.post('https://account-d.docusign.com/oauth/token',
{
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: token
})
.then(resposne => {
console.log(response);
})
.catch(err => {
if (err.response) {
console.log(err);
} else if (err.request) {}
else {}
})
But I am constantly getting error: { error: 'invalid_grant', error_description: 'no_valid_keys_or_signatures' }
I would suggest using the node.JS SDK or npm package and using the build-it JWT method to authenticate. The code would look like this:
(click here for GitHub example)
DsJwtAuth.prototype.getToken = async function _getToken() {
// Data used
// dsConfig.dsClientId
// dsConfig.impersonatedUserGuid
// dsConfig.privateKey
// dsConfig.dsOauthServer
const jwtLifeSec = 10 * 60, // requested lifetime for the JWT is 10 min
scopes = "signature", // impersonation scope is implied due to use of JWT grant
dsApi = new docusign.ApiClient();
dsApi.setOAuthBasePath(dsConfig.dsOauthServer.replace('https://', '')); // it should be domain only.
const results = await dsApi.requestJWTUserToken(dsConfig.dsClientId,
dsConfig.impersonatedUserGuid, scopes, rsaKey,
jwtLifeSec);
const expiresAt = moment().add(results.body.expires_in, 's').subtract(tokenReplaceMin, 'm');
this.accessToken = results.body.access_token;
this._tokenExpiration = expiresAt;
return {
accessToken: results.body.access_token,
tokenExpirationTimestamp: expiresAt
};
Recently implemented Asp.net core 2.0 WEB Api. Works smashingly on my local dev environment. HOWEVER ... when i deploy to AZURE i find that my JWT Access Token does NOT contain the Issuer and Audience claims and therefore i get the 401 Unauthorized with : Bearer error="invalid_token", error_description="The audience is invalid". The JWT generated on my local machine has : (courtesy of jwt.io)
{
"http://schemas.xmlsoap.org/...": "Rory#gspin.com",
"sub": "Rory#gspin.com",
"given_name": "Rory",
"family_name": "McGilroy",
"email": "Rory#gspin.com",
"jti": "3875f83d-eb93-4d45-8507-795a0cb7e3e4",
"iat": 1533506381,
"rol": "api_access",
"id": "420990b2-4747-4c3c-ae0f-ccbbc4dfe521",
"nbf": 1533506381,
"exp": 1533513581,
"iss": "gspin.com",
"aud": "https://www.gspin.com"
}
But after deploying same application to AZURE APP Service my Access Token contains the following:
{
"http://schemas.xmlsoap.org/...": "billyttom#fido.com",
"sub": "billyttom#fido.com",
"given_name": "billy mark tom",
"family_name": "last",
"email": "billyttom#fido.com",
"jti": "0d34a03f-31ae-45aa-9ace-004d5916b430",
"iat": 1533498384,
"rol": "api_access",
"id": "5485d641-974b-4f60-ade6-35c048503701",
"nbf": 1533498383,
"exp": 1533505583
}
Missing iss and aud???
Any idea why these are being dropped when deployed to azure when they are defined and present in Token generated on local machine/Visual Studio env?
My Code is : public async Task<string> GenerateEncodedToken(string
userName, ClaimsIdentity identity, UserManager<GSIdentityUser> _userManager)
{
var user = await _userManager.FindByNameAsync(userName);
var userClaims = await _userManager.GetClaimsAsync(user);
var claims = new[]
{
new Claim(ClaimTypes.Name, userName),
new Claim(JwtRegisteredClaimNames.Sub, userName),
new Claim(JwtRegisteredClaimNames.GivenName, user.FirstName),
new Claim(JwtRegisteredClaimNames.FamilyName, user.LastName),
new Claim(JwtRegisteredClaimNames.Email, user.Email), /// same as username
new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()), // the uniqueness claim is a GUID
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
identity.FindFirst(Helpers.Constants.Strings.JwtClaimIdentifiers.Rol),
identity.FindFirst(Helpers.Constants.Strings.JwtClaimIdentifiers.Id)
};
// Create the JWT security token and encode it.
var jwt = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
notBefore: _jwtOptions.NotBefore,
expires: _jwtOptions.Expiration,
signingCredentials: _jwtOptions.SigningCredentials);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
return encodedJwt;
}
public ClaimsIdentity GenerateClaimsIdentity(string userName, string id)
{
return new ClaimsIdentity(new GenericIdentity(userName, "Token"), new[]
{
new Claim(Helpers.Constants.Strings.JwtClaimIdentifiers.Id, id),
new Claim(Helpers.Constants.Strings.JwtClaimIdentifiers.Rol, Helpers.Constants.Strings.JwtClaims.ApiAccess)
});
}
Also in ConfigureServices i have :
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = Configuration["JwtIssuerOptions:Issuer"];
options.Audience=Configuration["JwtIssuerOptions:Audience"];
options.SigningCredentials = new SigningCredentials(_signingKey,
SecurityAlgorithms.HmacSha256);
});