Setup
msal (in another file. Passed using MsalProvider):
const msalInstance = new PublicClientApplication({
auth: {
clientId: <B2C-Application-ID>,
authority: "https://login.microsoftonline.com/<tenant-directory-id>",
redirectUri: "http://localhost:3000",
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
}
});
Import:
import * as msal from "#azure/msal-browser";
import {EventType, InteractionStatus} from "#azure/msal-browser";
import React, {createContext, FC, useState} from "react";
import {useIsAuthenticated, useMsal} from "#azure/msal-react";
import {AuthenticationContextType} from "../#types/authentication";
import {EndSessionRequest} from "#azure/msal-browser/dist/request/EndSessionRequest";
import jwtDecode, {JwtPayload} from "jwt-decode";
Variables:
const {instance, accounts, inProgress} = useMsal();
const isAuthenticated = useIsAuthenticated();
const [token, setToken] = useState<string | null>(null);
Login:
function loginRedirect() {
instance.loginRedirect({
scopes: ["User.Read"],
prompt: "select_account"
});
}
Acquire token:
function getToken(): string | null {
if (token) {
const decodedJwt = jwtDecode<JwtPayload>(token);
if (decodedJwt.exp && decodedJwt.exp * 1000 > Date.now()) {
return token; // Token is still valid
}
}
// If token is not available or not valid anymore, acquire a new one
if (instance.getActiveAccount() && inProgress === InteractionStatus.None) {
const accessTokenRequest = {
scopes: ["User.Read"],
account: accounts[0]
}
instance.acquireTokenSilent(accessTokenRequest)
.then(response => {
console.log(`access token: ${response.accessToken}`);
console.log(`id token: ${response.idToken}`);
setToken(response.accessToken);
return response.accessToken;
})
.catch(err => {
if (err instanceof msal.InteractionRequiredAuthError) {
return instance.acquireTokenPopup(loginRequest)
.then(response => {
setToken(response.accessToken);
return response.accessToken;
})
.catch(err => {
console.log(err);
})
} else {
console.log(err);
}
})
} else {
console.error("No account logged in to acquire token");
}
return null;
}
Problem
I acquire two tokens (ID and access) from msal (see console logs). The ID token is being validated successfully (on my API and jwt.io) but my access token is not (neither on my API nor jwt.io). Referring to this microsoft documentation I should use the access token to validate against an API.
As far as I can see, jwt.io does fetch the public key correctly from https://sts.windows.net/<tenant-directory-id>/discovery/v2.0/keys. This means this solution is either outdated, or doesn't solve my problem. To go sure I also tried to copy&paste the public key, which didn't work either.
I also found this solution which didn't work for me either. Changing the scopes leads to an endless login loop.
Versions:
"#azure/msal-browser": "^2.28.3",
"#azure/msal-react": "^1.4.7",
"jwt-decode": "^3.1.2",
1. Scope
For requesting B2C access tokens you have to specify a valid scope. These are also set in Azure (Azure AD B2C -> App registrations -> your application -> Manage -> API permissions). There you have to specify a scope. While acquiring the tokens you have to specify these scopes like this:
const accessTokenRequest = {
scopes: ["https://<tenant-name>.onmicrosoft.com/<app-id>/<scope>"],
}
await instance.acquireTokenSilent(accessTokenRequest)
.then(response => {
setIdToken(response.idToken);
setAccessToken(response.accessToken);
})
.catch(async err => {
if (err instanceof msal.InteractionRequiredAuthError) {
await instance.acquireTokenPopup(accessTokenRequest)
.then(response => {
setIdToken(response.idToken);
setAccessToken(response.accessToken);
})
.catch(err => {
console.log(err);
})
} else {
console.log(err);
}
})
tenant-name you can find this in the Application ID URI
app-id is your Application (client) ID
your-scope could be something like Subscriptions.Read
A full example for a scope could be:
https://mydemo.onmicrosoft.com/12345678-0000-0000-0000-000000000000/Subscriptions.Read
2. Invalid token version
For me the problem was 1. Scope but maybe this does not solve the problem for others. Here is something else to try:
Following this article, the sts url is used vor the v1 endpoint. The documentation claims:
The endpoint used, v1.0 or v2.0, is chosen by the client and only impacts the version of id_tokens. Possible values for accesstokenAcceptedVersion are 1, 2, or null. If the value is null, this parameter defaults to 1, which corresponds to the v1.0 endpoint.
This means that the used endpoint (v2.0 in my case) affected only the id-token, which made it validate successfully. The access token was still v1 thus with no validated signature.
Solution
To change the version, accessTokenAcceptedVersion needs to be set to 2 inside the Manifest. It is located at portal.azure.com -> Azure AD B2C -> App registrations -> your application -> Manage -> Manifest:
{
...
"accessTokenAcceptedVersion": 2,
...
}
Save the changes and wait. For me it took several hours to wait for the change to be applied. And I had to apply solution 1. Scope as well. After that, the iss of new tokens should be https://login.microsoftonline.com/<tenant-directory-id>/v2.0 instead of the sts-uri
Related
I am trying to use #azure/msal-node on a node backend server.
all work fine for business accounts onmicrosoft.com but not for personnal accounts like outlook.com
according to this documentation, Authentication seems to be possible
https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
But i don't understand if my problem come from azure AD configuration or from my code.
exp.post('/connect', function (req, res) {
let authCodeUrlParameters = {
scopes: SCOPES_OUTLOOK,
redirectUri: "http://localhost:4220/redirect",
};
publicMicrosoftClient.getAuthCodeUrl(authCodeUrlParameters).then((response) => {
if (req.body.email) {
response += `&login_hint=${req.body.email}`
}
open(response)
}).catch((error) => console.log(JSON.stringify(error)));
});
exp.get('/redirect', async function (req, res) {
try {
const form = {
'code': req.query.code,
'client_id': CLIENT_ID_OUTLOOK,
'scope': SCOPES_OUTLOOK.join(' '),
'redirect_uri': 'http://localhost:4220/redirect',
'grant_type': 'authorization_code',
'client_secret': encodeURI(SECRET_VALUE_OUTLOOK),
}
const options = {
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'post',
}
response = await got(options, { form });
respToken = response.body
...
} catch (error) {
console.log(error)
res.end();
}
});
the error come from ...v2.0/token request. the server response doesn't really help (error 400 bad request)
in azure AD we have app registered and all required scope with status granted.
Thank you in advance for your help,
Yan
• You are getting this error because you have not allowed or selected ‘Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)’ as shown below in the snapshot for the Azure AD app registered regarding authentication for your backend code: -
• Also, if you have already configured your Azure AD application registration for your node backend code, then you can also configure the above in your Azure AD app’s ‘Application Manifest’ by modifying the parameter ‘signInAudience’ with the value as ‘AzureADandPersonalMicrosoftAccount’ as well as ensure that the below parameters are also set as per the stated values to resolve this issue: -
“allowPublicClient” : true
“accesstokenAcceptedVersion” : 2
For more information regarding this, kindly refer to the below link: -
https://learn.microsoft.com/en-us/azure/active-directory/develop/reference-app-manifest
How to get access_token from next_auth to use it with googleapis,
lets say i am creating a crud app that store the data in google drive, I am using nextjs and next-auth for OAuth implementation for google. i found this blog so i implemented it. but it logs undefined.
src/pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import jwt from 'next-auth/jwt'
const secret = process.env.SECRET
export default NextAuth({
// Configure one or more authentication providers
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
authorization:{
params:{
scope:"openid https://www.googleapis.com/auth/drive.file"
}
}
}),
],
secret: process.env.SECRET,
callbacks: {
jwt: ({token, user, account, profile, isNewUser})=> {
console.log({token,user,account,profile})
if (account?.accessToken) {
token.accessToken = account.accessToken;
}
return token;
},
session: async ({session, user,token}) => {
session.user = user;
session.token = token;
return session
}
},
});
and I created a route with nextjs to get the access token
import {getToken,decode} from 'next-auth/jwt'
const handler = async(req, res)=> {
const secret = process.env.SECRET
const token = await getToken({ req, secret });
const accessToken = token.accessToken;
console.log(accessToken)
}
export default handler
any help would be great. thanks
the google's token is stored in account.access_token not account.accessToken. so the jwt callback must be
callbacks: {
jwt: ({token, account })=> {
if (account?.access_token) {
token.access_token = account.access_token;
}
return token;
},
},
and it is better not to expose tokens on clients side which I done in session callback. it is insecure.
As stated in the documentation, you must forward any data you want to be available in the token, such is your accessToken value:
The session callback is called whenever a session is checked. By default, only a subset of the token is returned for increased security. If you want to make something available you added to the token through the jwt() callback, you have to explicitly forward it here to make it available to the client.
So, you just have to add this to your session callback:
session.accessToken = token.accessToken;
I am trying to do silent login through msal.js and then trying to call graph api but always I get 403 error. When I decrypt my access token through jwt.ms I can see that audience is correct but scopes are showing wrong. Hope some can help me.
My code
let config = {
auth: {
clientId: _spPageContextInfo.spfx3rdPartyServicePrincipalId,
authority: `https://login.microsoftonline.com/${_spPageContextInfo.aadTenantId}`,
redirectUri: 'https://xxx.sharepoint.com/sites/xxx-Dev/Pages/myportal.aspx',
validateAuthority: false,
postLogoutRedirectUri: window.origin,
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: true
}
}
let myMSALObj = new Msal.UserAgentApplication(config)
let graphConfig = {
graphGroupEndpoint: "https://graph.microsoft.com/v1.0/groups"
}
let request = {
scopes: ["https://graph.microsoft.com/.default"]
}
myMSALObj.handleRedirectCallback(response => { console.log(response) });
//const idTokenScope = { scopes: [_spPageContextInfo.spfx3rdPartyServicePrincipalId] }
const handleError = (error) => {
if (error.errorCode === 'consent_required'
|| error.errorCode === 'interaction_required'
|| error.errorCode === 'login_required') {
//myMSALObj.loginRedirect(idTokenScope);
myMSALObj.loginRedirect(request);
return;
}
throw error;
};
const getToken = () => {
const date = new Date();
const user = myMSALObj.getAccount();
if (!user) {
//myMSALObj.loginRedirect(idTokenScope);
myMSALObj.loginRedirect(request);
return;
}
//myMSALObj.acquireTokenSilent(idTokenScope).then(response => {
myMSALObj.acquireTokenSilent(request).then(response => {
console.log(`${date.toLocaleTimeString()}`, response.accessToken);
callMSGraph(graphConfig.graphGroupEndpoint, response.accessToken, graphAPICallback)
}).catch(handleError);
}
getToken()
function callMSGraph(theUrl, accessToken, callback) {
var xmlHttp = new XMLHttpRequest()
xmlHttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200)
callback(JSON.parse(this.responseText))
}
xmlHttp.open("GET", theUrl, true)
xmlHttp.setRequestHeader('Authorization', 'Bearer ' + accessToken)
xmlHttp.send()
}
function graphAPICallback(data) {
document.write(JSON.stringify(data, null, 2))
}
My decoded token
My app permission
There are two kinds of permissions: one is application permission, the othre one is delegated permission. And "https://graph.microsoft.com/.default" is for application permisisons.
With interactively signning from browser, you will be asked to provided with your credentials. In this way, you will get an access token for yourself, which is with delegated permissions. We call this as OAuth 2.0 authorization code flow
However, as you do not set any required delegated permission in your request scope, Azure AD just return you an access token with basic delegated permissions (openid, email an profile).
By the way, if you just want to get an access token with application permissions. You just need to use the OAuth 2.0 client credentials flow to get a token.
I'm trying to automate all the testing of an API. Currently is using a utentificacion using AAD.
The problem is: I can use the process of postman to get the token using OAuth2.0
Postman dialog
but I can't run a collection and do something like a trigger to get the token at the beginning. If i want to take the token I must push the button "Get new access token"
there is some way to do it automatically? or how can I create a flow to obtain the token?
Thanks!
You could use Pre-request Script to do it automatically. You just need to modify your required value and post it in the Pre-request Script of the postman. It had better in the parent collection, so it could inherit auth from the parent.
var getToken = true;
if (!pm.environment.get('accessTokenExpiry') ||
!pm.environment.get('currentAccessToken')) {
console.log('Token or expiry date are missing')
} else if (pm.environment.get('accessTokenExpiry') <= (new Date()).getTime()) {
console.log('Token is expired')
} else {
getToken = false;
console.log('Token and expiry date are all good');
}
if (getToken === true) {
pm.sendRequest({
url: 'https://login.microsoftonline.com/microsoft.onmicrosoft.com/oauth2/token',
method: 'POST',
header: 'Content-Type:application/x-www-form-urlencoded',
body: {
mode: 'raw',
raw: 'grant_type=implicit&client_id...'
}
}, function (err, res) {
console.log(err ? err : res.json());
if (err === null) {
console.log('Saving the token and expiry date')
var responseJson = res.json();
pm.environment.set('currentAccessToken', responseJson.access_token)
var expiryDate = new Date();
expiryDate.setSeconds(expiryDate.getSeconds() + responseJson.expires_in);
pm.environment.set('accessTokenExpiry', expiryDate.getTime());
}
});
}
For the code sample, you could refer to here.
I would like to use auth0.com in conjunction with the open source-parse server.
My current approach is to obtain the token from auth0 by using their standard login through the Lock library for iOS. With that token I would like to call a custom authentication method on my parse-server, that checks whether the token is valid and if it is will log in the user.
My problem is that there is almost no documentation on writing custom oauth for parse-server.
So far, I have this code for my custom auth.
var Parse = require('parse/node').Parse;
function validateAuthData(authData, options) {
console.log('validateAuthData()');
return new Promise((resolve, reject) => {
try {
var decoded = jwt.verify(authData.access_token, opions.sharedSecret);
if (authData.id === decoded.sub) {
resolve({});
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Unauthorized');
} catch(e) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, e.message);
}
});
}
function validateAppId(appIds, authData) {
console.log('validateAppId()');
return Promise.resolve();
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData
};
However, it doesn't work and also I don't understand how this code can be used to authenticate a specific user. Does the parse-server do database look-ups to match the specific auth data to a specific user? Also, how can I register a new user with custom auth. What happens when a user tries to log in but he doesn't exist yet in my parse database?
An alternative seems to be this, using a rule an auth0.com. What are the differences and how would the rule work? I have very little experience with authentication and oauth and jwt's.
Lastly, I am using this to call my custom auth from my iOS client. However this doesn't work either, but I am not sure whether it is due to the iOS part or because my custom auth isn't working yet.
In conclusion, I am having trouble with something that seems rather easy. I want to use auth0 as my authentication provider and I want to integrate it was the parse-server, since I really appreciate the convenience around parse and the client sdk's. I am fairly certain that more people have a similar problem, however I have not found any definitive resource on how to properly do this.
Further Links
Parse user authenticated using Auth0
https://auth0.com/blog/2016/03/07/hapijs-authentication-secure-your-api-with-json-web-tokens/
https://github.com/ParsePlatform/parse-server/wiki/OAuth
https://jwt.io/introduction/
late answer but I was solving the same problem and came across this post:
Auth0 has rules you can apply that run when the login occurs. I've modified their example one from https://github.com/auth0/rules/blob/master/src/rules/parse.js, extracting the API endpoint into a constant.
function(user, context, callback) {
// run this only for the Parse application
// if (context.clientID !== 'PARSE CLIENT ID IN AUTH0') return callback(null, user, context);
const request = require('request');
const MY_API = 'https://subdomian.back4app.io';
const PARSE_APP_ID = '*********';
const PARSE_API_KEY = '**********';
const PARSE_USER_PASSWORD = 'REPLACE_WITH_RANDOM_STRING'; // you can use this to generate one http://www.random.org/strings/
const username = user.email || user.name || user.user_id; // this is the Auth0 user prop that will be mapped to the username in the db
request.get({
url: `${MY_API}/login`,
qs: {
username: username,
password: PARSE_USER_PASSWORD
},
headers: {
'X-Parse-Application-Id': PARSE_APP_ID,
'X-Parse-REST-API-Key': PARSE_API_KEY
}
},
function(err, response, body) {
if (err) return callback(err);
// user was found, add sessionToken to user profile
if (response.statusCode === 200) {
context.idToken[`${MY_API}/parse_session_token`] = JSON.parse(body).sessionToken;
return callback(null, user, context);
}
// Not found. Likely the user doesn't exist, we provision one
if (response.statusCode === 404) {
request.post({
url: `${MY_API}/users`,
json: {
username: username,
password: PARSE_USER_PASSWORD
},
headers: {
'X-Parse-Application-Id': PARSE_APP_ID,
'X-Parse-REST-API-Key': PARSE_API_KEY,
'Content-Type': 'application/json'
}
},
function(err, response, body) {
if (err) return callback(new Error('user already exists'));
// user created, add sessionToken to user profile
if (response.statusCode === 201) {
context.idToken[`${MY_API}/parse_session_token`] = body.sessionToken;
return callback(null, user, context);
}
return callback(new Error(username + ' The user provisioning returned an unknown error. Body: ' + JSON.stringify(body)));
});
} else {
return callback(new Error('The login returned an unknown error. Status: ' + response.statusCode + ' Body: ' + body));
}
});
}
I'm writing a SPA in JS, so I have some client side code that handles the Auth0 login, (replace 'https://subdomian.back4app.io' with your own parse server's API address - the same value as used in the above Auth0 rule). Note the Parse.User.become function, which assigns the session id created in the Auth0 rule to the current parse User:
handleAuthentication() {
this.auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult);
Parse.User.become(authResult.idTokenPayload['https://subdomian.back4app.io/parse_session_token']);
history.replace('/');
} else if (err) {
history.replace('/home');
console.log(err);
}
});
}