I hope you can help me: I currently develop an app which needs access to the users calendar (outlook-calendar) to find free meeting slots (other users will be able to see and then select one of the free slots - similar to calendly). For that I use msal-node to authenticate against azureAD. But my use case needs "everytime"-access to the calendars from all users. This is why I want to get an refresh_token. The docs of msal-node say that I should provide the offline_scope to get an refreshtoken while doing the OAuth-process.
My problem is that I receive an access_token and id_token and so on, but no refreshtoken. The Azure-response further shows a successful answer but when I take a look into the returned scopes I cannot find offline_scope.
You can see the returned scopes here
What should I do?
I use a cofidentalClientApplication msal-node instance:
const oauth2Client = MicrosoftClient.Connection
const authCodeUrlParameters = {
scopes: ["offline_access", "user.read"],
forceRefresh: true,
redirectUri: "http://localhost:3000/outlookRedirect",
}
try {
console.log("GDFHGJF")
return oauth2Client.getAuthCodeUrl(authCodeUrlParameters)
}
After receiving the code from Azure, I process it via:
const oauth2Client = MicrosoftClient.Connection
const tokenRequest = {
code: code,
scopes: ["user.read", "offline_access"],
forceRefresh: true,
redirectUri: "http://localhost:3000/outlookRedirect",
//client_secret: process.env.MICROSOFTCLIENTSECRET,
}
const testus = await oauth2Client.acquireTokenByCode(tokenRequest)
const tokenRequest2 = {
scopes: ["user.read", "offline_access"],
forceRefresh: true,
redirectUri: "http://localhost:3000/outlookRedirect",
account: testus.account,
}
oauth2Client
.acquireTokenSilent(tokenRequest2)
.then((response) => {
console.log("\nResponse: \n:", response)
})
.catch((error) => {
console.log(error)
})
return
What is my fault? I appreciate any kind of help!
Thank you in advance,
Lukas
after calling 'acquireTokenByCode' , 'pca' now has the refresh token. const tokenCache = pca.getTokenCache().serialize(); const refreshTokenObject = (JSON.parse(tokenCache)).RefreshToken const refreshToken = refreshTokenObject[Object.keys(refreshTokenObject)[0]].secret;
Below is a complete snippet of How to get the Refresh and Access token.
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
const express = require("express");
const msal = require('#azure/msal-node');
const SERVER_PORT = process.env.PORT || 3000;
const REDIRECT_URI = "http://localhost:3000/redirect";
// Before running the sample, you will need to replace the values in the config,
// including the clientSecret
const config = {
auth: {
clientId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
authority: "https://login.microsoftonline.com/84fb56d3-e15d-4ae1-acd7-cbf83c4c0af3",
clientSecret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose,
}
}
};
// Create msal application object
const pca = new msal.ConfidentialClientApplication(config);
// Create Express App and Routes
const app = express();
app.get('/', (req, res) => {
const authCodeUrlParameters = {
scopes: ["user.read","offline_access"],
redirectUri: REDIRECT_URI,
prompt:'consent'
};
// get url to sign user in and consent to scopes needed for application
pca.getAuthCodeUrl(authCodeUrlParameters).then((response) => {
res.redirect(response);
}).catch((error) => console.log(JSON.stringify(error)));
});
app.get('/redirect', (req, res) => {
const tokenRequest = {
code: req.query.code,
scopes: ["user.read","offline_access"],
redirectUri: REDIRECT_URI,
accessType: 'offline',
};
pca.acquireTokenByCode(tokenRequest).then((response) => {
const accessToken = response.accessToken;
const refreshToken = () => {
const tokenCache = pca.getTokenCache().serialize();
const refreshTokenObject = (JSON.parse(tokenCache)).RefreshToken
const refreshToken = refreshTokenObject[Object.keys(refreshTokenObject)[0]].secret;
return refreshToken;
}
const tokens = {
accessToken,
refreshToken:refreshToken()
}
console.log(tokens)
res.sendStatus(200);
}).catch((error) => {
console.log(error);
res.status(500).send(error);
});
});
app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`))
msal-node does not expose the refresh token to the end user by design. It is stored and used internally under the hood when you need a new access token. You should call acquireTokenSilent each time you need an access token and msal-node will manage the tokens by either returning a cached token to you or using the refresh token to acquire a new access token.
For more context: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/2836
In addition to the accepted answer, its important to note that the MSAL cache can have many authenticated users (and lots of refresh tokens). Here is my solution to extract the exact refresh token for a specific user.
I use this on each login to exact the individuals refresh token and store it.
public extractRefresh = ( homeAccountId : string ) : string =>
{
try
{
const tokenCache = this.msalClientApp.getTokenCache().serialize();
const refreshTokenObject = ( JSON.parse( tokenCache ) ).RefreshToken;
let refreshToken = '';
Object.entries( refreshTokenObject ).forEach( ( item : any ) =>
{
if ( item[1].home_account_id === homeAccountId )
{
refreshToken = item[1].secret;
}
});
return refreshToken;
}
catch
{
return '';
}
}
Related
I am using firebase functions for instagram authentication for my android app.
This is my flow:
User access my "connect" api with param that states its user name (..... ?username=someusername)
"Connect" saves the username as a cookie called "__session" (as this is the only cookie allowed by functions).
"connect" redirects 'https://api.instagram.com' which asks the user for permission to give me their details.
After user agrees, Instagram redirects to my function called "onredirect"
I store the user token in realtime data base along side with the user name found in "__session" cookie.
All of this worked all in well for a month or two but suddenly "redirect" function can no longer retrieve the "__session" cookie (it's empty).
I'm not sure what has changed or possibly what im doing wrong (it used to work and the code has not changed), would be happy for you help.
Code:
exports.connectinsta = functions.https.onRequest(async (req, res) => {
functions.logger.log('connectinsta query:', req.query);
functions.logger.log('connectinsta query:', req.query.account);
if(!req.query.account){
res.send("No user account provided in query")
} else {
cookieParser()(req, res, () => {
const oauth2 = instagramOAuth2Client2();
res.setHeader('Cache-Control', 'private');
const redirectUri = oauth2.authorizationCode.authorizeURL({
redirect_uri: OAUTH_REDIRECT_URI_Y,
scope: OAUTH_SCOPES,
client_id: functions.config().instagram.client_id,
});
res.cookie('__session', req.query.account, {
maxAge: 3600000,
secure: true,
httpOnly: true,
});
res.redirect(redirectUri)
});
}
});
exports.onredirect = functions.https.onRequest(async (req, res) => {
try {
cookieParser()(req, res, async () => {
functions.logger.log('onredirect cookies:', req.cookies);
functions.logger.log('onredirect cookies session:', req.cookies.__session);
const session_id = req.cookies.__session <--- NO LONGER WORKS
const oauth2 = instagramOAuth2Client();
const results = await oauth2.authorizationCode.getToken({
client_id: functions.config().instagram.client_id,
client_secret: functions.config().instagram.client_secret,
grant_type: "authorization_code",
code: req.query.code,
redirect_uri: OAUTH_REDIRECT_URI_Y
});
const accessToken = results.access_token;
functions.logger.log('EExperiment:', accessToken);
var result_long = await get_long_token_promise(accessToken)
functions.logger.log('Long EExperiment:', result_long.access_token);
save_code(session_id, result_long.access_token)
res.send("Success! Return to app")
});
} catch(error) {
functions.logger.error("onredirect error1", error)
return res.jsonp({
error: error.toString(),
});
}
});
I'm using the msal nodejs library. I have the following code
const ouathClient = new msal.ConfidentialClientApplication(msalConfig);
const tokenRequest = {
code: request.query.code,
scopes: process.env.OUTLOOK_OAUTH_SCOPES.split(','),
redirectUri: process.env.DOMAIN_NAME + "/outlook/oauth/redirect",
accessType: "offline"
};
const response = await ouathClient.acquireTokenByCode(tokenRequest);
const accessToken = response.accessToken;
const refreshToken = () => {
const tokenCache = ouathClient.getTokenCache().serialize();
const refreshTokenObject = (JSON.parse(tokenCache)).RefreshToken
const refreshToken = refreshTokenObject[Object.keys(refreshTokenObject)[0]].secret;
return refreshToken;
}
const tokens = {
accessToken,
refreshToken: refreshToken()
}
IS this how to get the refresh token from the msal-node library? I created an app that connects doctors and patients. I want patients to be able to book time on a doctor's outlook calendar. I need to get access to the doctor's outlook account. I can use the access token to get access to his calendar, but that expires.
How do I refresh the token after some time?
I am going to get my subscription list with the help of the YouTube api. I wrote this piece of code.
const { google } = require('googleapis');
const oauth2 = google.oauth2('v2');
const express = require('express')
const app = express()
const port = 3000
const oauth2Client = new google.auth.OAuth2(
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"xxxxxxxxxxxxxxxxxxxxxxx",
"http://localhost:3000/auth/google/callback"
);
google.options({
auth: oauth2Client
});
// generate a url that asks permissions for Blogger and Google Calendar scopes
const scopes = [
'https://www.googleapis.com/auth/youtube',
'https://www.googleapis.com/auth/youtube.channel-memberships.creator'
];
const url = oauth2Client.generateAuthUrl({
// 'online' (default) or 'offline' (gets refresh_token)
access_type: 'offline',
// If you only need one scope you can pass it as a string
scope: scopes
});
app.get('/',(req,res,next)=>{
res.send(url);
})
let tok = "";
app.get('/auth/google/callback',(req,res,next)=>{
res.send('Susses Authrations');
console.log("Code authrations : "+req.query.code);
const {tokens} = oauth2Client.getToken(req.query.code)
oauth2Client.setCredentials(tokens);
oauth2Client.on('tokens', (tokens) => {
if (tokens.refresh_token) {
// store the refresh_token in my database!
console.log("refresh_token : "+ tokens.refresh_token);
tok = tokens.access_token;
}
console.log("Assess Token : "+ tokens.access_token);
});
})
app.get('/youtube',(req,res,next)=>{
const youtube = google.youtube('v3',{
'access_token':oauth2Client.credentials.access_token,
'refresh_token':oauth2Client.credentials.refresh_token,
'api_key':oauth2Client.credentials.api_key
});
youtube.channels.list({
"part": [
"snippet,contentDetails,statistics"
],
"id": [
"UC_x5XG1OV2P6uZZ5FSM9Ttw"
]
}).then(e=>{
console.log(e.request)
})
})
app.listen(port,()=>{
console.log("Hello World")
});
But unfortunately I encounter an error (Error: No access, refresh token or API key is set.) Which apparently does not recognize my refresh token. I am a novice and thank you for guiding me. I also use the clinet id and clinet secret I also built a console inside Google and activated YouTube related libraries.
To give a bit of context: I am writing an API to serve a internal CMS in React that requires Google login and a React Native app that should support SMS, email and Apple login, I am stuck on what way of authentication would be the best, I currently have an example auth flow below where a team member signs in using Google, a refresh token gets sent in a httpOnly cookie and is stored in a variable in the client, then the token can be exchanged for an accessToken, the refresh token in the cookie also has a tokenVersion which is checked before sending an accessToken which does add some extra load to the database but can be incremented if somebody got their account stolen, before any GraphQL queries / mutations are allowed, the user's token is decoded and added to the GraphQL context so I can check the roles using graphql-shield and access the user for db operations in my queries / mutations if needed
Because I am still hitting the database even if it's only one once on page / app load I wonder if this is a good approach or if I would be better off using sessions instead
// index.ts
import "./passport"
const main = () => {
const server = fastify({ logger })
const prisma = new PrismaClient()
const apolloServer = new ApolloServer({
schema: applyMiddleware(schema, permissions),
context: (request: Omit<Context, "prisma">) => ({ ...request, prisma }),
tracing: __DEV__,
})
server.register(fastifyCookie)
server.register(apolloServer.createHandler())
server.register(fastifyPassport.initialize())
server.get(
"/auth/google",
{
preValidation: fastifyPassport.authenticate("google", {
scope: ["profile", "email"],
session: false,
}),
},
// eslint-disable-next-line #typescript-eslint/no-empty-function
async () => {}
)
server.get(
"/auth/google/callback",
{
preValidation: fastifyPassport.authorize("google", { session: false }),
},
async (request, reply) => {
// Store user in database
// const user = existingOrCreatedUser
// sendRefreshToken(user, reply) < send httpOnly cookie to client
// const accessToken = createAccessToken(user)
// reply.send({ accessToken, user }) < send accessToken
}
)
server.get("/refresh_token", async (request, reply) => {
const token = request.cookies.fid
if (!token) {
return reply.send({ accessToken: "" })
}
let payload
try {
payload = verify(token, secret)
} catch {
return reply.send({ accessToken: "" })
}
const user = await prisma.user.findUnique({
where: { id: payload.userId },
})
if (!user) {
return reply.send({ accessToken: "" })
}
// Check live tokenVersion against user's one in case it was incremented
if (user.tokenVersion !== payload.tokenVersion) {
return reply.send({ accessToken: "" })
}
sendRefreshToken(user, reply)
return reply.send({ accessToken: createAccessToken(user) })
})
server.listen(port)
}
// passport.ts
import fastifyPassport from "fastify-passport"
import { OAuth2Strategy } from "passport-google-oauth"
fastifyPassport.registerUserSerializer(async (user) => user)
fastifyPassport.registerUserDeserializer(async (user) => user)
fastifyPassport.use(
new OAuth2Strategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:4000/auth/google/callback",
},
(_accessToken, _refreshToken, profile, done) => done(undefined, profile)
)
)
// permissions/index.ts
import { shield } from "graphql-shield"
import { rules } from "./rules"
export const permissions = shield({
Mutation: {
createOneShopLocation: rules.isAuthenticatedUser,
},
})
// permissions/rules.ts
import { rule } from "graphql-shield"
import { Context } from "../context"
export const rules = {
isAuthenticatedUser: rule()(async (_parent, _args, ctx: Context) => {
const authorization = ctx.request.headers.authorization
if (!authorization) {
return false
}
try {
const token = authorization.replace("Bearer", "")
const payload = verify(token, secret)
// mutative
ctx.payload = payload
return true
} catch {
return false
}
}),
}
To answer your question directly, you want to be using jwts for access and that's it. These jwts should be created tied to a user session, but you don't want to have to manage them. You want a user identity aggregator to do it.
You are better off removing most of the code to handle user login/refresh and use a user identity aggregator. You are running into common problems of the complexity when handling the user auth flow which is why these exist.
The most common is Auth0, but the price and complexity may not match your expectations. I would suggest going through the list and picking the one that best supports your use cases:
Auth0
Okta
Firebase
Cognito
Authress
Or you can check out this article which suggests a bunch of different alternatives as well as what they focus on
I,ve been using google-auth-library#0.10.0 nodejs just for verifying user identity in my api services, now it changed to 1.0 and everything is broken.
I previously used example from here:
https://developers.google.com/identity/sign-in/web/backend-auth
now I cannot figure out how to verify identity using the new library.
Examples here: https://github.com/google/google-auth-library-nodejs
explains how to get access to google apis, I just need to verify identity.
this is my code:
const GoogleAuth = require('google-auth-library');
const auth = new GoogleAuth.GoogleAuth();
const google = require('googleapis');
const authData = {
'googleAuth': {
'clientID': 'xxxxxxxxxxx-aaaaaaaaaaaaaaaaaaaaaa.apps.googleusercontent.com',
'clientSecret': 'sssssssssssssssssssssssss',
'callbackURL': 'http://localhost:121212/auth/'
}
};
const CLIENT_ID = authData.googleAuth.clientID;
function verifyToken(token) {
let tokenPromise = new Promise(function(resolve, reject) {
client.verifyIdToken(
token,
CLIENT_ID,
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3],
function(e, login) {
if (e) {
return reject(e);
} else {
var payload = login.getPayload();
var userid = payload['sub'];
//console.log(payload, userid, e, login);
return resolve(login);
// If request specified a G Suite domain:
//var domain = payload['hd'];
}
});
});
return tokenPromise;
};
it was working fine, just to get googleUserId.
now I've this error:
Error: This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry.
I understand I'm not passing properly parameters and maybe its not the method to use..but how can I know which method and how it accepts parameters??? I cannot find any documentation about google-auth-library#1.0...some one is using it in production??
The verifyIdToken changed. Here's a working example.
import { OAuth2Client } from 'google-auth-library';
const client = new OAuth2Client(
'xxxxxxxxxxx-aaaaaaaaaaaaaaaaaaaaaa.apps.googleusercontent.com,
'sssssssssssssssssssssssss',
'http://localhost:121212/auth/',
);
client.verifyIdToken({
idToken: TOKEN,
audience: CLIENT_ID_1 // If you have multiple [CLIENT_ID_1, CLIENT_ID_2, ...]
}, (err, login) => {
console.log(login);
});
Just tested this recently.
Google needs to update their documentation.
Please note that you can specify a maxAge to the verify id token options as well.
Here's the interface copied from the source code
export interface VerifyIdTokenOptions {
idToken: string;
audience: string|string[];
maxExpiry?: number;
}
As you can see the idToken and audience are required, the maxExpiry is optional.
Source can be found here