I have used Pusher Beams in my app, it has worked great till 2 days ago. It has stopped with this error:
Could not set user id, jwt rejected: Forbidden: Invalid JWT issuer
Any idea how to solve this? and is it related to server side or is in the android?
fun setPusherBeam(userId: String){
PushNotifications.start(context, BuildConfig.INSTANCE_ID)
val tokenProvider = BeamsTokenProvider(
BuildConfig.PUSHER_BEAM,
object : AuthDataGetter {
override fun getAuthData(): AuthData {
return AuthData(
headers = hashMapOf(
"Authorization" to auth_token_key)
),
queryParams = hashMapOf()
)
}
}
)
PushNotifications.setUserId(
userId,
tokenProvider,
object : BeamsCallback<Void, PusherCallbackError> {
override fun onFailure(error: PusherCallbackError) {
Timber.d("Beams login failed: ${error.message}")
}
override fun onSuccess(vararg values: Void) {
Timber.d("Beams login success ")
}
}
)
} catch (ex: Exception) {
Timber.d("Beams ex ${ex.localizedMessage}")
}
}
}
It seems that your jwt token doesn't have the same issuer claim anymore so your token provider, PusherBeam in this case, can't validate the token and because of that it fails.
To check if anything of your token has changed, you can go to https://jwt.io website, paste your jwt token, and see if the iss claim (issuer) has a value different from the one your token provided expects.
If this is not the problem you should check the other claims also (exp to be sure that the token is not expired, aud to be sure that contains the service that must recieve the token... ) But reading the error the problem must be with the iss claim
Related
I have a nestJS backend protected with Auth0. I am able to successfully access the backend from react-admin by including the access token in the authorization header (Authorization: Bearer ACCESS_TOKEN)
However, I seem to have an issue when trying to access the same backend from a Swift iOS app. I have followed the Auth0 tutorials and am able to confirm successful user login and access to user profile. However, when I try to make a request to the nestJS backend, I receive a 401 Unauthorized error. Interestingly, nothing is recorded in the Auth0 logs.
Link to Auth0 tutorial: https://auth0.com/docs/quickstart/native/ios-swift/04-calling-apis
let path = "\(baseURL)\(endpoint.rawValue)"
guard let url = URL(string: path)
else { preconditionFailure("Bad URL") }
var headers: [String:String] = [:]
headers["Content-Type"] = "application/json"
// if access token is set then set Authorization headers
if (accessToken != nil) {
headers["Authorization"] = "Bearer \(accessToken!)"
print("Bearer \(accessToken!)")
}
var request = URLRequest(url: url)
request.httpMethod = "\(method)"
request.allHTTPHeaderFields = headers
// check if body exists
if (body != nil) {
request.httpBody = body!
}
let dataTask = URLSession.shared.dataTask(with: request) {
(data, response, error) in
guard error == nil
else { completion(.failure(.serverError)); return }
do {
guard let data = data
else { completion(.failure(.serverError)); return }
guard let object : [[String: AnyObject]] = try JSONSerialization.object(with: data) as? [[String: AnyObject]]
else {
print("Unable to convert from data")
return
}
guard let json = try? JSONSerialization.data(withJSONObject: object, options: .prettyPrinted)
else {
print("Unable to prettify")
return
}
guard let jsonString = String(data: json, encoding: .utf8)
else {
print("Unable to convert to string")
return
}
print("JSON: \(jsonString)")
completion(Result.success(object))
} catch {
completion(Result.failure(.parsingError))
}
}
dataTask.resume()
baseURL is a string that points to my nestJS backend.
endpoint is an enum of endpoints, for example \user
Using Proxyman I am able to confirm that the endpoint is hit with the correct headers. Screenshot below.
Additionally, using postman I am able to successfully login and also make a get request to protected data. Screenshot below.
Any ideas to what might be the cause? Let me know if I should add any additional details.
UPDATE
I decoded the successful (react-admin) and unsuccessful (iOS) JWT tokens and noticed the following differences:
aud in the successful JWT contains an array of audiences that include the API registerd on Auth0 as well as an auth0 endpoint https://xxxxx.us.auth0.com/userinfo
azp is only present in the successful JWT and contains my clientID
aud in the unsuccessful token contains the clientID
scope and permissions is missing from unsuccessful token.
Ps. also posted on Auth0 Community
https://community.auth0.com/t/access-token-when-obtained-from-ios-results-in-401-unauthorized-while-from-react-admin-is-ok/71115
The problem was that audience was set when requesting access tokens from the react-admin, while I did not include this in the swift login implementation.
Decoding the JWT on jwt.io and the following thread lead to this conclusion.
https://community.auth0.com/t/access-token-too-short-jwt-malformed/9169/11?u=kennethphough
Adding the audience in the following code resulted in the correct jwt being returned and successful access to backend.
Auth0
.authentication()
.login(
usernameOrEmail: self.email,
password: self.password,
realm: "Username-Password-Authentication",
audience: "<YOUR_AUDIENCE>", // <- This is what I forgot
scope: "openid profile email"
)
In example project provided by Microsoft here which uses Authorization code flow the acquireTokenByCode method does not return refresh tokens.
From #azure/msal-node here refresh token is not mentioned.
Result returned from the authority's token endpoint.
uniqueId - oid or sub claim from ID token
tenantId - tid claim from ID token
scopes - Scopes that are validated for the respective token
account - An account object representation of the currently signed-in user
idToken - Id token received as part of the response
idTokenClaims - MSAL-relevant ID token claims
accessToken - Access token received as part of the response
fromCache - Boolean denoting whether token came from cache
expiresOn - Javascript Date object representing relative expiration of access token
extExpiresOn - Javascript Date object representing extended relative expiration of access token in case of server outage
state - Value passed in by user in request
familyId - Family ID identifier, usually only used for refresh tokens
please ensure your MSAL authorization code request includes the offline_access scope.
You could use MSAL.js to get token in this case, there is acquireTokenSilent method, it can perform silent renewal of tokens, which means you are no need to get the refresh token by yourself.
Popup
var request = {
scopes: ["Mail.Read"]
};
msalInstance.acquireTokenSilent(request).then(tokenResponse => {
// Do something with the tokenResponse
}).catch(async (error) => {
if (error instanceof InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return myMSALObj.acquireTokenPopup(request);
}
}).catch(error => {
handleError(error);
});
Redirect
var request = {
scopes: ["Mail.Read"]
};
msalInstance.acquireTokenSilent(request).then(tokenResponse => {
// Do something with the tokenResponse
}).catch(error => {
if (error instanceof InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return myMSALObj.acquireTokenRedirect(request)
}
});
It's designed to not return the refresh token if you are using #azure/msal-node.
As they stated in the discussion, the refresh token is handled background, inside the library itself for better security, which I also disagree with.
However, if you insist to have the token, you can manually call the API to the AzureAD endpoint.
I'm using implicit grant type, and when I request "id_token token" as response type my HttpContext.Current.User is null after logging in leading me to believe something has gone wrong inside owin. If I just have "id_token" as response type its fine. Do I need to tell owin somewhere to get the access token?
For reference I'm using .Net Framework as my client and identityserver4.
To be able to get the token via browser you need to set AllowAccessTokensViaBrowser = true on client's config within IdentityServer:
new Client
{
...
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
...
},
and on MVC client's Startup, to you can add the access_token as a claim to user:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
...
ResponseType = "id_token token",
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
n.AuthenticationTicket.Identity.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
return Task.FromResult(0);
}
}
});
I have the full working sample here
Scenario:
I have an angular5 client application, which uses hello.js to authenticate users using their office 365 credentials.
Client Code:
hello.init({
msft: {
id: configuration.AppID,
oauth: {
version: 2,
auth: 'https://login.microsoftonline.com/' + configuration.TenantID + '/oauth2/v2.0/authorize'
},
scope_delim: ' ',
form: false
},
},
{ redirect_uri: configuration.redirecturl }
);
}
login() {
hello('msft').login({ scope: 'User.Read People.Read', display: 'popup' })
.then((authData: any) => { // console.log(authData);
this.zone.run(() => {
// get profile
}
A successful response is (Manipulated for security reasons)
{
"msft":{
"access_token":"REMOVED TOKEN HERE",
"token_type":"Bearer",
"expires_in":3599,
"scope":"basic,User.Read,People.Read",
"state":"",
"session_state":"3b82898a-2b3f-445363f-89ae-d9696gg64ad3",
"client_id":"672330148-2bb43-3080-9eee-1f46311f789c",
"network":"msft",
"display":"popup",
"redirect_uri":"http://localhost:5653/",
"expires":15245366.218
}
}
The decoded access_token has these few keys:
Header:
1. nonce (requires some special processing, I couldn't find any documentation regarding special processing)
2. typ: JWT
PayLoad:
"aud": "https://graph.microsoft.com",
Once the access_token is received, I am sending the access_token in authorization header of every call to my backend API. The goal is to validate the token and only send a successful response if the access_token is validated and authorized. If unsuccessful, 401 Unauthorized is the response.
API Code to validate access_token, ASP .NET CORE 2, Following (https://auth0.com/blog/securing-asp-dot-net-core-2-applications-with-jwts/)
namespace JWT
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void ConfigureServices(IServiceCollection services)
{
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.AddMvc();
}
}
}
// other methods
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseMvc();
}
In appsettings.json I have:
{ "Jwt": {
"Key": "verySecretKey", **(I got the key from https://login.microsoftonline.com/common/discovery/keys with the kid value in access_token header)**
"Issuer": "https://sts.windows.net/49bcf059-afa8-4bf9-8470-fad0c9cce27d/", } }
Finally, the error I receive is :
"WWW-Authenticate →Bearer error="invalid_token", error_description="The signature key was not found""
I have been stuck here since past few days, any help will be life savior.
Key Points:
I tried to validate the access_token in jwt.io (https://nicksnettravels.builttoroam.com/post/2017/01/24/Verifying-Azure-Active-Directory-JWT-Tokens.aspx) but I was not able to.
The aud here is https://graph.microsoft.com, I am not sure if I need to and why do I need to change aud to my client id. how do I do that?
Is there something wrong in the code or do i need to tweak the way I am requesting header tokens.
Please let me know if you need more information.
I tried to validate the access_token in jwt.io (https://nicksnettravels.builttoroam.com/post/2017/01/24/Verifying-Azure-Active-Directory-JWT-Tokens.aspx) but I was not able to.
Microsoft Graph API access tokens are signed differently from other access tokens from what I can see.
You do not need to validate tokens that are meant for another API, it is their job.
The aud here is https://graph.microsoft.com, I am not sure if I need to and why do I need to change aud to my client id. how do I do that?
I don't know about HelloJS, but you should be able to get an Id token after authentication with response_type=id_token token.
Then you need to attach that to the requests.
It should have your client id as the audience.
Is there something wrong in the code or do i need to tweak the way I am requesting header tokens.
The only thing that stands out to me is that you are doing a lot of unnecessary configuration.
Basically the configuration should be:
.AddJwtBearer(o =>
{
o.Audience = "your-client-id";
o.Authority = "https://login.microsoftonline.com/your-tenant-id/v2.0";
})
The handler will automatically fetch the public signing keys on startup.
It's not really a good idea to hard-code signing keys in your app since your app will break when AAD finishes signing key rollover.
I also spent a lot of time trying to validate it, but the bottom line is that you can't:
Access tokens are opaque blobs of text that are for the resource only. If you're a client getting a token for Graph, assume that it's an encrypted string that you should never look at - sometimes it will be. We use a special token format for Graph that they know how to validate - you shouldn't be looking at access tokens if they're not for you. (source: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/609)
Instead of using the access token, you should create an ID token, which is a regular JWT token that can be validated like any other JWT:
Get the public key from the Microsoft directory
Validate the signature, audience, issuer, etc.
To get an ID token using the MSAL API after login you can do (javascript example):
const { instance, accounts } = useMsal();
const request = {
scopes: ["User.Read"],
account: accounts[0]
};
const idToken = await instance.acquireTokenSilent(request).idToken;
For more information on ID Tokens, please check:
https://learn.microsoft.com/en-us/azure/active-directory/develop/id-tokens
For more information on opaque tokens, please check:
https://zitadel.com/blog/jwt-vs-opaque-tokens
Yeah, this took a bit to work through. For anyone else researching this, here's my understanding.
You don't use the Microsoft Graph API to secure your web api. Instead:
The client continues to use the Microsoft Identity Platform to authenticate.
The client uses the resulting JWT access token to call the Web API as normal for OAuth 2.0 flow
The web API uses JwtBearerAuthenticationScheme, setting the authority to the Microsoft identity platform. See this example and search for JwtBearerAuthenticationScheme.
The web API uses the provided access token to obtain an 'On Behalf Of' user token.
The web API calls the Graph API using this 'On Behalf Of' token. This token has a different lifespan than the token the client obtained, and refreshes must be handled separately.
This is a very distilled version of this example. Disclaimer: I haven't put this into practice yet.
my current Authentication process looks like this. I have a Auth API that generates a token with UseOAuthBearerAuthentication. Once I generate the token inside GrantResourceOwnerCredentials I set the Identity.Name by identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.UserName));
I want to store my token in a HttpOnly cookie, so in TokenEndpointResponse(OAuthTokenEndpointResponseContext context)
I saved it to a cookie, but I also need to prevent XSRF so I generate a XSRF token there as well.
string cookieToken, formToken;
AntiForgery.GetTokens(null, out cookieToken, out formToken);
If I look at the current Identity context from OAuthTokenEndpointResponseContext context, Identity.Name is set, so it should be using this name for the XSRF token generation. But HttpContext.Current.User.Identity is null In my both my APIs I also set
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimsIdentity.DefaultNameClaimType;
Now In my Resource API, I've already taken care of reading the auth token from the cookie and setting it to the Authorization header. That works fine. I have then created my own XSRF Attribute the verify the XSRF token.
public class ValidateAntiForgery : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
try
{
string cookieToken = "";
string formToken = "";
IEnumerable<string> tokenHeaders;
if (actionContext.Request.Headers.TryGetValues("X-XSRF-TOKEN", out tokenHeaders))
{
string[] tokens = tokenHeaders.First().Split(':');
if (tokens.Length == 2)
{
cookieToken = tokens[0].Trim();
formToken = tokens[1].Trim();
}
}
AntiForgery.Validate(cookieToken, formToken);
}
catch(Exception e)
{
actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Bad Request");
}
}
}
Checking the Identity in actionContext, everything is set and the user is authenticated. However, AntiForgery.Validate(cookieToken, formToken); throws an exception as shown below. I've looked at the many other examples, but I can't find a solution.
System.Web.Mvc.HttpAntiForgeryException: 'The provided anti-forgery
token was meant for a different claims-based user than the current
user.'
EDIT: So it seems where I generate my XSRF token in TokenEndpointResponse(OAuthTokenEndpointResponseContext context)
context.Identity is set with the authenticated User, however HttpContext.Current.User is null. Even though the XSRF tokens generated here are technically valid, I think they are using null from the HttpContext. If I generate the XSRF token is a separate GET request with [Authorize] so that HttpContext.Current.User is not null, then the AntiForgery.Validate works fine.
I want the XSRF token to be returned with the Authentication token, but I'm not sure how to do that. How do I set HttpContext.Current.User.Identity?
EDIT 2: So I was able to fix the problem using a hacky way. When I want to generate the XSRF token I call the following function.
[Authorize]
public string generateXSRFToken(ClaimsIdentity identity)
{
HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(identity, new string[0]);
string cookieToken, formToken;
AntiForgery.GetTokens(null, out cookieToken, out formToken);
return cookieToken + ":" + formToken;
}
It works!, but it's ugly, I would like a more elegant way.
This should fix your problem:
var returnUrl = Request.QueryString["ReturnUrl"];
if (returnUrl.IsEmpty()) {
// Some external login providers always require a return URL value
returnUrl = Href("~/");
}
if (WebSecurity.Login(email, password, rememberMe))
{
Context.RedirectLocal(returnUrl);
return;
}
else
{
ModelState.AddFormError("The user name or password provided is incorrect.");
}