I have an application registered on my Azure AD tenant. When I receive the access token after logging in I send the token to my node.js API. I'm trying to validate the signature but I keep getting "invalid signature" error. The JWT header lists the specific key that it supposedly used to sign the token. So why can't I verify it? Is there something I need to tweak in the AAD configuration? Are there options I need to specify that I pass to azure-ad-jwt? I've tried passing in { audience: 'https://graph.windows.net'}. I've also tried passing in the audience that is in the decoded token as well as the specific kid that is listed in the header so that it only tries the specific key that the token says it is signed with.
This is my code that is giving me the error:
const aad = require('azure-ad-jwt');
module.exports = (req, res, next) => {
const jwtToken = req.headers.authorization.replace('Bearer ', '');
aad.verify(jwtToken, null, function (err, result) {
if (result) {
console.log("JWT is valid");
next();
} else {
console.log("JWT is invalid: " + err);
res.status(401).json({
message: "Auth failed"
});
}
});
};
If your front-end is acquiring a token for audience https://graph.windows.net, then that token is not meant for your API.
It is a token for Azure AD Graph API.
You should not be validating that.
Instead, your front-end needs to specify your API app's client id or app ID URI as the resource (if using the v1 endpoint/ADAL) or valid scopes defined by your API (if using the v2 endpoint/MSAL).
Then you will get a token you can validate.
Related
Can someone help me understand how to add a permission to a MS-Graph API call?
I'm trying to create a new team/group but I'm getting a permission error. Obviously I need add the Team.Create permission if I want to create a Team. Which I did as seen in the image below
Here's the sample code where I tried to add the permission to the MSAL client request:
// Initialize Graph client
const client = graph.Client.init({
// Implement an auth provider that gets a token
// from the app's MSAL instance
authProvider: async (done) => {
try {
// Get the user's account
const account = await msalClient
.getTokenCache()
.getAccountByHomeId(userId);
let scope = process.env.OAUTH_SCOPES.split(',');
scope.push("Team.Create");
console.log("Added a extra permission request");
console.log("scope = " + scope);
if (account) {
// Attempt to get the token silently
// This method uses the token cache and
// refreshes expired tokens as needed
const response = await msalClient.acquireTokenSilent({
scopes: scope,
redirectUri: process.env.OAUTH_REDIRECT_URI,
account: account
});
console.log("\nResponse scope = " + JSON.stringify(response) + "\n");
// First param to callback is the error,
// Set to null in success case
done(null, response.accessToken);
}
} catch (err) {
console.log(JSON.stringify(err, Object.getOwnPropertyNames(err)));
done(err, null);
}
}
});
return client;
Then I get the following error:
The user or administrator has not consented to use the application with ID 'xxxxxxx'
named 'Node.js Graph Tutorial'. Send an interactive authorization request for this user and resource
I did give permissions to Team.Create in the Azure Active Directory, so how do I consent to this app gaining access? Note this code is the tutorial for learning Graph: https://learn.microsoft.com/en-us/graph/tutorials/node
Judging by the screenshot, you can't give admin consent to the permission as it is grayed out.
You'll need to try if you can grant user consent.
acquireTokenSilent won't work in this case since consent is needed.
You need to use one of the interactive authentication methods to trigger user authentication, at which time you can consent to the permission on your user's behalf.
In that sample specifically, you probably need to modify the scopes here: https://github.com/microsoftgraph/msgraph-training-nodeexpressapp/blob/08cc363e577b41dde4f6a72ad465439af20f4c3a/demo/graph-tutorial/routes/auth.js#L11.
And then trigger the /signin route in your browser.
We are using Google AppEngine (Java) with Extensible Service Proxy (ESP) as our backend, and Auth0 as our authenticator.
We can successfully invoke Auth0 from our web app and authenticate using email/password, GoogleAccount and FaceBook and receive an id_token and an access_token. As expected when decoded via https://jwt.io/ we can see that the
id_token contains contains (iss, sub, aud, iat, exp) as well as email address and other user info.
access_token contains all the fields required by EndPoints (iss, sub, aud, iat, exp) but no email address.
When we invoke our AppEngine EndPoint providing the access_token as a bearer token, the endpoint gets invoked but the User is always null. There are no errors in the AppEngine logs.
We had expected that the ESP would validate the access_token and provide our AppEngine method with the validated User containing the credentials from Auth0.
Is our expectation incorrect, or have we misconfigured something?
What do we need to do to receive a User authorised by Auth0 in our AppEngine method?
#Api(
name = "ourInterface",
version = "v1",
namespace = #ApiNamespace(ownerDomain = "our-domain.com", ownerName = "Our Company"),
authenticators = {EspAuthenticator.class},
// Authenticate using Auth0
issuers = {
#ApiIssuer(
name = "auth0",
issuer = "https://our-domain.au.auth0.com/",
jwksUri = "https://our-domain.au.auth0.com/.well-known/jwks.json")
},
issuerAudiences = {
#ApiIssuerAudience(
name = "auth0",
audiences = "https://our-domain.appspot.com/ourInterface" // The interface specified in Applications\APIs in Auth0
)
}
)
public class OurInterface {
#ApiMethod(name = "postSomeStuff", path = "postSomeStuff", httpMethod =HttpMethod.POST)
public SomeResponse postSomeStuff(SomeRequest request, User user) {
If (user == null) {
throw new IllegalStatException(“Expected a User”);
}
// Take user and request params and confirm User has access to those resources
// Then do something useful
}
}
Update 1
If we add
authLevel = AuthLevel.REQUIRED,
to the Api annotation, then our method is not invoked and the ESP fails the request with
com.google.api.server.spi.SystemService invokeServiceMethod: exception occurred while calling backend method
com.google.api.server.spi.response.UnauthorizedException: Valid user credentials are required.
at com.google.api.server.spi.request.ServletRequestParamReader.deserializeParams(ServletRequestParamReader.java:161)
at com.google.api.server.spi.request.RestServletRequestParamReader.read(RestServletRequestParamReader.java:161)
at com.google.api.server.spi.SystemService.invokeServiceMethod(SystemService.java:347)
at com.google.api.server.spi.handlers.EndpointsMethodHandler$RestHandler.handle(EndpointsMethodHandler.java:127)
at com.google.api.server.spi.handlers.EndpointsMethodHandler$RestHandler.handle(EndpointsMethodHandler.java:110)
at com.google.api.server.spi.dispatcher.PathDispatcher.dispatch(PathDispatcher.java:50)
at com.google.api.server.spi.EndpointsServlet.service(EndpointsServlet.java:80)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:848)
So I'm trying to follow the security best practices and I'm sending my JWT token over my React app in a only-secure http-only cookie.
This works fine for requests but the major issue I find with this approach is, how can I tell if the user is logged-in on client-side if I can't check if the token exists? The only way I can think of is to make a simple http to a protected endpoint that just returns 200.
Any ideas? (not looking for code implementations)
The approach I would follow is to just assume the user is logged in, and make the desired request, which will send the httpOnly token automatically in the request headers.
The server side should then respond with 401 if the token is not present in the request, and you can then react in the client side accordingly.
Using an endpoint like /api/users/me
Server-side
Probably you don't only need to know if a user is already logged in but also who that user is. Therefore many APIs implement an endpoint like /api/users/me which authenticates the request via the sent cookie or authorization header (or however you've implemented your server to authenticate requests).
Then, if the request is successfully authenticated, it returns the current user. If the authentication fails, return a 401 Not Authorized (see Wikipedia for status codes).
The implementation could look like this:
// UsersController.ts
// [...]
initializeRoutes() {
this.router.get('users/me', verifyAuthorization(UserRole.User), this.getMe);
}
async getMe(req: Request, res: Response) {
// an AuthorizedRequest has the already verified JWT token added to it
const { id } = (req as AuthorizedRequest).token;
const user = await UserService.getUserById(id);
if (!user) {
throw new HttpError(404, 'user not found');
}
logger.info(`found user <${user.email}>`);
res.json(user);
}
// [...]
// AuthorizationMiddleware.ts
export function verifyAuthorization(expectedRole: UserRole) {
// the authorization middleware throws a 401 in case the JWT is invalid
return async function (req: Request, res: Response, next: NextFunction) {
const authorization = req.headers.authorization;
if (!authorization?.startsWith('Bearer ')) {
logger.error(`no authorization header found`);
throw new HttpError(401, 'unauthorized');
}
const token = authorization.split(' ')[1];
const decoded = AuthenticationService.verifyLoginToken(token);
if (!decoded) {
logger.warn(`token not verified`);
throw new HttpError(401, 'unauthorized');
}
(req as AuthorizedRequest).token = decoded;
const currentRole = UserRole[decoded.role] ?? 0;
if (currentRole < expectedRole) {
logger.warn(`user not authorized: ${UserRole[currentRole]} < ${UserRole[expectedRole]}`);
throw new HttpError(403, 'unauthorized');
}
logger.debug(`user authorized: ${UserRole[currentRole]} >= ${UserRole[expectedRole]}`);
next();
};
}
Client-side
If the response code is 200 OK and contains the user data, store this data in-memory (or as alternative in the local storage, if it doesn't include sensitive information).
If the request fails, redirect to the login page (or however you want your application to behave in that case).
I am attempting to integrate Azure AD login and Graph API into my angular2 website.
I have successfully implemented an ADAL login and redirect, built around a useful blog post here
From this I retrieved an id_token parameter that my adalservice can access. Currently this is acheived through a simple context.login() and catching the token in the redirect.
When I use this token to try and access Microsoft Graph, I receive an InvalidAuthenticationToken response stating Access Token validation failure.
I'm new to this stuff, so it could be that my call is intrinsically wrong, or that I lack certain permissions in AD, or my app reg lacks permissions. I've seen that I potentially need to request an access token with sufficient scope, yet I can find any examples of this.
Has anyone used this adalService library to obtain tokens for use with Graph API?
I found a solution to my problem.
I was using the wrong token. I had to acquire a token specifically for Graph API. This meant I would have to first log in and then call this.context.acquireToken() like below:
this.context.acquireToken("https://graph.microsoft.com", function (error, id_token) {
if (error || !id_token) {
console.log('ADAL error occurred: ' + error);
}
else {
this.graphAccessToken = id_token;
//Call graph API
}
}.bind(this)
);
It seems like it's essential that this process have 2 calls. Maybe someone can shed some light on whether I can immediately obtain a token with scope for the Graph API on login. Perhaps by setting required permissions for the app in Azure AD.
Just to have a clarity for all, updating the end to end solution here again.
In case you do not have the base starter code, refer to this link Adal-JS Tutorial. This post only concerns with the customization involved.
Step 1: Configure the AdalService
(only new code is shown, other methods remain as it is)
export class AdalService {
public get graphAccessToken() {
return sessionStorage[new AppConstants().User_Graph_Token];
}
public retrieveTokenForGraphAPI() {
this.context.acquireToken('https://graph.microsoft.com', function (error, graph_token) {
if (error || !graph_token) {
console.log('ADAL error occurred: ' + error);
} else {
// Store token in sessionStorage
sessionStorage[new AppConstants().User_Graph_Token] = graph_token;
return;
}
}.bind(this)
);
}
}
The code should have existing handlers for id_token callback and corresponding configuration in the routing. If not, please refer to link above for the initial code.
Now the requirement is retrieve the access_token once the id_token is retrieved. The access_token has additional field for "puid" which describes identifier for claims. This will be the step 2.
Step 2: Update LoginComponent
ngOnInit() {
if (!this.adalService.isAuthenticated) {
console.log('LoginComponent::Attempting login via adalService');
this.adalService.login();
} else {
if (this.adalService.accessTokenForGraph == null) {
console.log('LoginComponent::Login valid, attempting graph token retrieval');
this.adalService.retrieveTokenForGraphAPI();
}
}
Now the token is retrieved and stored for later use.
Step 3: Update Routing for 'access_token' callback
Similar to the 'id_token' callback, we need to add additional callback route for the access_token. The callback components will remain same. Their code is as described in the main link. Note that *access_token" endpoint is MS provided, hence be careful not to change the name.
{ path: 'access_token', component: OAuthCallbackComponent, canActivate: [OAuthCallbackHandler] },
{ path: 'id_token', component: OAuthCallbackComponent, canActivate: [OAuthCallbackHandler] }
Step 4: Use the token wherever required
const bearer = this.adalService.graphAccessToken();
When I place the below in a React component that queries the user model I am able to get the entire user object as queried by graphQL including the id property:
console.log(this.props.data.user)
But when I try to access the id property from code:
console.log(this.props.data.user) // undefined
console.log(this.props.data.user.id)
// error cannot get property of undefined
My first guess is that this is a security feature; I am using Auth0 and Graphcool.
But I may just be going about this in a backwards way. If so, any help on accessing the user id in the correct manner would be appreciated.
Thanks
This is covered in the FAQ article on the logged in user.
Obtaining a Signed JWT with Auth0
The user query returns the currently authenticated user. So first we have to think about how the authenticated user is determined.
The current state-of-the-art is using verified JWT and passing them as the Authorization header.
After entering valid credentials in Auth0 Lock, it returns a JWT that is signed with your secret Auth0 key. This signed JWT is sent to the GraphQL server where we'll use your Auth0 key to verify it and if it belongs to a valid user, the request is authenticated.
Setting the Authorization Header with Apollo Client
So I suspect that you're simply not passing a valid Authorization header. With Apollo, you can use this to ensure passing the token if it is present. Note that we'll use local storage for storing the token from Auth0 Lock:
const networkInterface = createNetworkInterface({ uri: 'https://api.graph.cool/simple/v1/__PROJECT_ID__' })
// use the auth0IdToken in localStorage for authorized requests
networkInterface.use([{
applyMiddleware (req, next) {
if (!req.options.headers) {
req.options.headers = {}
}
// get the authentication token from local storage if it exists
if (localStorage.getItem('auth0IdToken')) {
req.options.headers.authorization = `Bearer ${localStorage.getItem('auth0IdToken')}`
}
next()
},
}])
const client = new ApolloClient({ networkInterface })
Check this Auth0 example code or the live demo to see how it works.
You might also be interested in this answer on authorization (permissions).