How to validate a self-issued JWT? - oauth-2.0

I was looking at the section about Self-issued OpenID Provider Response, where they describe a method of validation, where the public key is included in the token itself. They use this as an example token:
{
"iss": "https://self-issued.me",
"sub": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs",
"aud": "https://client.example.org/cb",
"nonce": "n-0S6_WzA2Mj",
"exp": 1311281970,
"iat": 1311280970,
"sub_jwk": {
"kty":"RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx
4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs
tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2
QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI
SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb
w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB"
}
}
I get how you can use the appended key for validation. But I don't get what prevents someone from using a fake key-pair to create a similar token. The only way I see this happening is if the public key is known by the validator from somewhere else, but in that case it doesn't make a lot of sense to include it in the token.
How does this work?

The way this work is that the hackers don't have access to the private key that was used to sign the token signature. The public key is derived from the private key.
The public key is safe to distribute as it only verifies the token signature. T
I don't think it's that common to include the public key inside the token as the token size gets bigger. Instead, you, as a receiver, download it separately, or it is provided to you some other way.
The picture below gives a summary of how public-key cryptography works.

But I don't get what prevents someone from using a fake key-pair to create a similar token.
Given the validations in Self-Issued ID Token Validation there is no feasible way for "someone" to sign a token which would have the same sub (JWT Subject). Of course they may sign a Token which would pass validations, but ONLY for a different subject. So IF you choose to accept Self-Issued ID Tokens, the guarantee you get is that you can re-identify the same Subject. That's kind of the whole point, Self-Issued OpenID providers are personal wallets, given the cryptography present in the flow you can be sure a given returned subject is the same one you encountered prior as long as sub is the same as before (or one that you know and have established trust with OOB).
Node.js code as a reference:
import * as assert from "node:assert";
import * as jose from "jose";
let jwt;
const redirect_uri = "https://rp.example.com/siop/cb";
const nonce = "n-0S6_WzA2Mj";
{
// this is unreachable by the party verifying
const kp = await jose.generateKeyPair("ES256");
const sub_jwk = await jose.exportJWK(kp.publicKey);
jwt = await new jose.SignJWT({ sub_jwk, nonce })
.setSubject(await jose.calculateJwkThumbprint(sub_jwk))
.setIssuer("https://self-issued.me")
.setAudience(redirect_uri)
.setProtectedHeader({ alg: "ES256" })
.setExpirationTime("5m")
.setIssuedAt()
.sign(kp.privateKey);
}
const verified = await jose.jwtVerify(
jwt,
async (protectedHeader, token) => {
const { sub_jwk, sub } = JSON.parse(
new TextDecoder().decode(jose.base64url.decode(token.payload))
);
assert.equal(sub === (await jose.calculateJwkThumbprint(sub_jwk)), true);
const key = await jose.importJWK(sub_jwk, protectedHeader.alg);
assert.equal(key.type, "public");
return key;
},
{
audience: redirect_uri,
issuer: "https://self-issued.me",
}
);
assert.equal(verified.payload.nonce, nonce);
console.log(verified);

Related

Access token validation fails if scope is graph.microsoft.com

Received access token from AAD, using below url
https://login.microsoftonline.com/gdfdddddd-87dd-497c-b894-xxxxxx/oauth2/v2.0/token
grant_type :client_credentials
client_id :xxxxx-1ff5-4615-8d71-yyyyyy
client_secret:[7aCw]fdsfsfsfds.AC61Fg:cm33
scope : https://vault.azure.net/.default
Validated the above received token using below code manually & it works fine
IConfigurationManager<OpenIdConnectConfiguration> configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>("https://login.microsoftonline.com/TestDomain310320.onmicrosoft.com/v2.0/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdConfig = AsyncHelper.RunSync(async () => await configurationManager.GetConfigurationAsync(CancellationToken.None));
TokenValidationParameters validationParameters =
new TokenValidationParameters
{
ValidIssuer = "https://sts.windows.net/a3d2bff3-87dd-497c-b894-f63befdd7496/",
ValidAudiences = new[] { "https://vault.azure.net" },
IssuerSigningKeys = openIdConfig.SigningKeys
};
SecurityToken validatedToken;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
var user = handler.ValidateToken(token, validationParameters, out validatedToken);
Modified parameter Scope:https://graph.microsoft.com/.default & received AAD token successfully but token validation using above code fails with error message "IDX10511: Signature validation failed. Keys tried: '[PII is hidden]'." Verified AAD app with above mentioned client id is having "user.read/user.read.basicall permissions". Why token validation fails if tokens are received from AAD with Scope:https://graph.microsoft.com/.default
Observation:
Token received with scope : https://vault.azure.net/.default
{
"typ": "JWT",
"alg": "RS256",
"x5t": "YMELHT0gvb0mxoSDoYfomjqfjYU",
"kid": "YMELHT0gvb0mxoSDoYfomjqfjYU"
}
While token received with Scope:https://graph.microsoft.com/.default has extra nonce property to avoid replay attack, is it be the reason for token validation failure?
{
"type": "JWT",
"nonce": "wCXLm9rF5Nma2S7OswU44uAVRpVbM_20WrWJkqbWe6Y",
"alg": "RS256",
"x5t": "YMELHT0gvb0mxoSDoYfomjqfjYU",
"kid": "YMELHT0gvb0mxoSDoYfomjqfjYU"
}
please suggest.
You should not be looking into, or validating tokens that were not issued to your own Apis. The intended receiver, KeyVault and MS Graph will do the necessary validation themselves. You should treat these Access Tokens as an opaque blobs that you stuff into the Authorization header in your calls to these Apis.
An Api owner, Graph or KeyVault can tomorrow can change the claims present in them or even choose to encrypt their tokens and your code will break.
Why are you validating tokens? If you are reading validated tokens of Apis that do not belong to you in your applications as a proof of Authentication, then you are setting yourself up for failure. Also its a security concern as any app in the world which can obtain an Access token for KeyVault or MS graph can pass it your Apis and compromise it.
Here is a discussion for reference - Cannot validate signature. #609
Yes, the error was caused by the nonce field in JWT header.
As far as I know, if we request the access token of graph api, the JWT token will contain the nonce field. And then we can't validate it on our backend(For security reasons, microsoft doesn't allow us to do this operation).

New Apple ID Sign In invalid grant issue

So far I followed this post and it helped me so much, however, I now get a "invalid_grant".
Following : https://developer.apple.com/documentation/signinwithapplerestapi/errorresponse I understand that I have an issue either because of the authorization grant or refresh token is invalid.
In despite of my searches and tries (and retries), I am still stuck and I don't know where does it come from. I used the app given at https://developer.apple.com/documentation/authenticationservices/adding_the_sign_in_with_apple_flow_to_your_app
Now that I get my token from the app above, I try to validate it from C# backend but I get a 400 response code invalid_grant.
The only difference I could notice from the post is that I don't have any [Verify] button (option) or [Download] button from the portal compared to the image below. I don't know if this is related but I am trying to provide as much details as I can:
Hopefully someone can help, thanks for any help :) feel free to ask for more details if required
Max
I also had the same issue, I found the solution here:
https://forums.developer.apple.com/thread/118135
as explained in the link, when you are using the code you got from the app, you should use app id instead of service id.
Could you share how you try to create the JWT?
I ve tried a couple of stuff Im at this right know (which doesnt work either, Ill update if I find a real solution)
const string iss = "7#######G"; // team ID
const string aud = "https://appleid.apple.com";
const string sub = "com.######.weblogin"; // serviceid
const string privateKey = "MIGTA#######"; // contents of .p8 file
var d = DateTime.UtcNow.AddDays(-5);
var cngKey = CngKey.Import(
Convert.FromBase64String(privateKey),
CngKeyBlobFormat.Pkcs8PrivateBlob);
var handler = new JwtSecurityTokenHandler();
var securityKey = new ECDsaSecurityKey(new ECDsaCng(cngKey) { KeySize = 256 , HashAlgorithm = CngAlgorithm.ECDsaP256});
securityKey.KeyId = "G#######W";
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);
return handler.CreateEncodedJwt(iss, aud, new ClaimsIdentity(new List<Claim> { new Claim("sub", sub) }),d, expires: d.AddMonths(3),d, signingCredentials: signingCredentials);
Headers look like that in the jwt, from what Ive gathered there might be the "typ" header which is not present in many implentation, perhaps I shoud get rid of it :
{
"alg": "ES256",
"kid": "G#######W",
"typ": "JWT"
}
body:
{
"sub": "com.#####.weblogin",
"nbf": 1583088895,
"exp": 1591037695,
"iat": 1583088895,
"iss": "7######G",//teamid
"aud": "https://appleid.apple.com"
}

Reproducing an ADAL.JS-authenticated request in Postman

I have a .NET Web API and a small vanilla-JS app using ADAL.js, and I've managed to make them talk nicely to each-other and authenticate correctly.
If I console.log the token returned from adalAuthContext.acquireToken() and manually enter it as Authorization: Bearer {{token}} in Postman, I can also get a valid, authenticated, response from my backend.
However, I can't figure out how to configure Postman's built-in OAuth2.0 authentication UI to get me tokens automatically. I have managed to get tokens in several ways, but none of them are accepted by the backend.
How do I configure Postman to get a token the same way the ADAL.js library does?
For completeness, here's some code:
Backend configuration:
public void Configuration(IAppBuilder app)
{
app.UseCors(CorsOptions.AllowAll);
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters { ValidAudience = "<app-id>" },
Tenant = "<tenant>",
AuthenticationType = "WebAPI"
});
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
ADAL.js configuration:
const backendUrl = 'http://localhost:55476';
const backendAppId = '<app-id>';
const authContext = new AuthenticationContext({
clientId: backendAppId,
tenant: '<tenant>',
endpoints: [{ [backendAppId]: backendAppId }],
cacheLocation: 'localStorage'
});
Actually making a request:
authContext.acquireToken(backendAppId, (error, token) => {
// error handling etc omitted
fetch(backendUrl, { headers: { Authorization: `Bearer ${token}` } })
.then(response => response.json())
.then(console.log)
})
So since the Azure AD v1 endpoint is not fully standards-compliant, we have to do things in a slightly weird way.
In Postman:
Select OAuth 2.0 under Authorization
Click Get new access token
Select Implicit for Grant Type
Enter your app's reply URL as the Callback URL
Enter an authorization URL similar to this: https://login.microsoftonline.com/yourtenant.onmicrosoft.com/oauth2/authorize?resource=https%3A%2F%2Fgraph.microsoft.com
Enter your app's application id/client id as the Client Id
Leave the Scope and State empty
Click Request token
If you configured it correctly, you'll get a token and Postman will configure the authorization header for you.
Now about that authorization URL.
Make sure you specify either your AAD tenant id or a verified domain name instead of yourtenant.onmicrosoft.com.
Or you can use common if your app is multi-tenant.
The resource is the most important parameter (and non-standards-compliant).
It tells AAD what API you want an access token for.
In this case I requested a token for MS Graph API, which has a resource URI of https://graph.microsoft.com.
For your own APIs, you can use either their client id or App ID URI.
Here is a screenshot of my settings:

Adding data to JWT from the client-side

I'm new using JWTs. I have an API that generates a JWT for the clients to be authenticated for further requests. My JWT has a property that returns the user id:
{
jwt: {
exp: "2017-12-12 00:00:00",
data: {
user_id: 491
}
}
}
My question is if the client can decode the JWT generated by the API and add a new property into the data field, like this:
{
jwt: {
exp: "2017-12-12 00:00:00",
data: {
user_id: 491,
status: 1
}
}
}
Or, if I can generate the JWT from the API auth system with the status field set to a default value and then the client could change it.
Thank you.
The client can do that, but it would make the token invalid. When you change the content of the payload, e.g. add another field or change its content, the signature of the token no longer matches. When the API receives a token with invalid signature it should reject the token. Imagine if you had a field called isAdmin and the client could change it from false to true. It would make your authentication pointless; the client doesn't decide whether it's admin or not, the backend does.
When the token's payload changes the signature has to be remade. In order to sign the token, the client has to know the secret key (for H256). But the client shouldn't know the secret key.
So the answer is no, the client can't change the token.
You can read more about that here.
In other words, you want to tamper with your JWT token and you cannot do it without invalidating the token.
The signature is calculated over the header and over the payload. The token issuer (the server) checks the signature to verify that the content has not been changed along the way.
In the latest Version of JWT Auth
$token = JWTAuth::claims(['account_id' => $account->id])->fromUser($user);
to data from token :
$payload = JWTAuth::getPayload();
$accountId = $payload->get('account_id');
in the preview version of JWT auth
$token = JWTAuth::fromUser($user, ['account_id' => $account->id]);
to data from token :
$payload = JWTAuth::getPayload(JWTAuth::getToken());

Using Bearer Authentication with Azure Active Directory Access Tokens

I am using the Passport AAD project with the bearer strategy to protect my endpoints. After I receive tokens with the OIDC strategy when logging in, I can't seem to get the bearer strategy to validate the signature of the access token. I get:
authentication failed due to: invalid signature
I have no problems validating the id_token, but I would prefer not to use this for our client app if the id_token can't be refreshed with AAD. Also, when using jwt.io to test the validation with the published public keys, I see the same issue (can validate the id_token, but not the access_token).
Am I missing a step when grabbing the access token, or is there a gap in my understanding of how access tokens are validated?
Update with more details
I am requesting an access token from my tenant:
identityMetadata: https://login.microsoftonline.com/your_tenant_name.onmicrosoft.com/.well-known/openid-configuration,
responseType: 'id_token code'
Using the OIDCStrategy in the AAD Passport project.
const callbackOIDC = (iss, sub, profile, accessToken, refreshToken, params, done) => {
return done(null,{
profile,
accessToken,
refreshToken
});
};
passport.use(new OIDCStrategy(config.creds, callbackOIDC));
Then I run authenticate, shown below:
auth.adCallback = function (req, res, next) {
passport.authenticate('azuread-openidconnect', {
response: res,
resourceURL: 'https://graph.microsoft.com',
session: false
}, function (err, user, info) {
console.log(user.access_token);
})(req, res, next);
};
I think I may have been asking for a graph access token above by specifying the resource URL. If I remove that resource URL, I still get an access token, but the bearer strategy throws an invalid token error (instead of an invalid signature error). Is there a different resource URL I should be setting to match with my tenant and get the access token I'm looking for?
What access tokens are you requesting? If the access token is meant to be used against the Microsoft Graph, for example, it is the Graph's task to validate them- not your app's.
Can you expand on the exact scenario you are trying to implement, and at what point you need to refresh id_tokens?

Resources