auth0 checkSession({}) returns login_required when logged in through social provider, but not when logging in via username/password - auth0

I have an Angular app that uses Auth0 for authentication, and I'm trying to use checkSession({}, …) to persist a user's session if the token hasn't expired yet.
When I log in with my username/pw that I set up for the site, this works fine when I reload the browser/navigate directly to a resource. However, when I log in using a social provider (such as Google), the checkSession({}, …) call on a page reload returns an error and forces the user to log in again.
Some of the relevant code (mostly from the auth0 tutorial(s)):
export class AuthService {
// Create Auth0 web auth instance
private _auth0 = new auth0.WebAuth({
clientID: AUTH_CONFIG.CLIENT_ID,
domain: AUTH_CONFIG.CLIENT_DOMAIN,
responseType: 'token',
redirectUri: AUTH_CONFIG.REDIRECT,
audience: AUTH_CONFIG.AUDIENCE,
scope: AUTH_CONFIG.SCOPE
});
accessToken: string;
userProfile: any;
expiresAt: number;
// Create a stream of logged in status to communicate throughout app
loggedIn: boolean;
loggedIn$ = new BehaviorSubject<boolean>(this.loggedIn);
loggingIn: boolean;
isAdmin: boolean;
// Subscribe to token expiration stream
refreshSub: Subscription;
constructor(private router: Router) {
// If app auth token is not expired, request new token
if (JSON.parse(localStorage.getItem('expires_at')) > Date.now()) {
this.renewToken();
}
}
...
handleAuth() {
// When Auth0 hash parsed, get profile
this._auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken) {
window.location.hash = '';
this._getProfile(authResult);
} else if (err) {
this._clearRedirect();
this.router.navigate(['/']);
console.error(`Error authenticating: ${err.error}`);
}
this.router.navigate(['/']);
});
}
private _getProfile(authResult) {
this.loggingIn = true;
// Use access token to retrieve user's profile and set session
this._auth0.client.userInfo(authResult.accessToken, (err, profile) => {
if (profile) {
this._setSession(authResult, profile);
this._redirect();
} else if (err) {
console.warn(`Error retrieving profile: ${err.error}`);
}
});
}
private _setSession(authResult, profile?) {
this.expiresAt = (authResult.expiresIn * 1000) + Date.now();
// Store expiration in local storage to access in constructor
localStorage.setItem('expires_at', JSON.stringify(this.expiresAt));
this.accessToken = authResult.accessToken;
this.userProfile = profile;
if (profile) {
this.isAdmin = this._checkAdmin(profile);
}
...
}
...
get tokenValid(): boolean {
// Check if current time is past access token's expiration
return Date.now() < JSON.parse(localStorage.getItem('expires_at'));
}
renewToken() {
// Check for valid Auth0 session
this._auth0.checkSession({}, (err, authResult) => {
if (authResult && authResult.accessToken) {
this._getProfile(authResult);
} else {
this._clearExpiration();
}
});
}
}
(This is from a service that is called in many places within the app, including some route guards and within some components that rely on profile information. If more of the app code would be useful, I can provide it.)
Also note: AUTH_CONFIG.SCOPE = 'openid profile email'

So, the issue appears to not have been related to my app at all. When using Social Providers, Auth0 has an explicit note in one of their tutorials that really helped me out:
The issue with social providers is that they were incorrectly configured in my Auth0 dashboard, and needed to use provider-specific app keys.
Important Note: If you are using Auth0 social connections in your app,
please make sure that you have set the connections up to use your own
client app keys. If you're using Auth0 dev keys, token renewal will
always return login_required. Each social connection's details has a
link with explicit instructions on how to acquire your own key for the
particular IdP.
Comment was found on this page: https://auth0.com/blog/real-world-angular-series-part-7/

Related

Why isn't NextAuth jwt automatically saved in cookies?

I have two simple authentication callback functions "jwt" and "session" which check if the user object exists and create the session if so.
callbacks: {
jwt: async ({ token, user }) => {
if(user) {
token.id = user.id
}
return token
},
session: ({ session, token }) => {
if(token) {
session.id = token.id
}
return session
},
}
My issue is, and I have been searching a lot to find information concerning this, why isn't this jwt automatically saved to cookies?
I find that my session is created and I am successfully "logged in", however if I look into my local storage there are no jwt cookies saved.
Do I have to manually save the jwt to my cookies in the jwt callback? Or is the jwt cookie not even required in the case of jwt session strategy? I need jwt cookies because from what I've read about middleware most solutions use cookies and decrypt the jwt to see if the user is logged in, instead of checking the getSession() hook.
You might need to to explain your problem in more detail since I can´t really tell what you already implemented and left out for simplicity sake. I hope this helps you anyway:
The steps to add the cookie look roughly like this:
Create / sign a jwt with a npm package like jose or jsonwebtoken
Set the header of your response and add your signed jwt to it; return it to the client
import { SignJWT, jwtVerify, JWTVerifyResult } from "jose";
async function setCookie(response, user: {id: number}) {
const token = await generateJwtToken(user);
response.setHeader("Set-Cookie", [
`user=${token};` +
"expires=" + new Date(new Date().getTime() + 1 * 86409000).toUTCString() + ";"
]);
response.status(200).json({message: "Successfully set cookie"})
}
async function generateJwtToken(user: { id: number }) {
return await new SignJWT({id: user.id})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("24h")
.sign(new TextEncoder().encode(process.env.YOUR_JWT_TOKEN_ENV_VAR));
}
Verify the jwt on further requests with the same package as in 1.
export async function verifyJwt(request) {
const token = request.cookies["yourCustomUser"];
const verified: JWTVerifyResult = await jwtVerify(
token,
new TextEncoder().encode(process.env.YOUR_JWT_TOKEN_ENV_VAR),
);
verified.payload.status = 200;
return verified.payload;
}
In addition to that you might wanna add sameSite=Strict, secure or path=/. For further information you should have a look at developers.mozilla
Also make sure to add error handling for expired Jwts etc.

How to debug invalid_grant during authorization code exchange?

My application is using OAuth to access the Youtube Data API. My OAuth callback is written in node and uses the OAuth2Client class from the "googleapis" npm package to exchange the authorization code for the access and refresh tokens.
Everything was working fine up to last week until suddenly I started getting the "invalid_grant" response during the authorization code exchange. I have tried everything to resolve this and am running out of ideas. My callback executes as a cloud function so I don't think that it would be out of sync with NTP.
My OAuth consent screen is in "Testing" mode and my email address is included in the test users. The odd thing is that even though the authorization code exchange fails, my Google account's "Third-party apps with account access" section lists my application as if the handshake succeeded.
Here is my code for generating the auth URL and exchanging the authorization code. The invalid_grant occurs during the call to "oauth2.getToken"
async startFlow(scopes: string[], state: string): Promise<AuthFlow> {
const codes = await oauth2.generateCodeVerifierAsync();
const href = oauth2.generateAuthUrl({
scope: scopes,
state,
access_type: 'offline',
include_granted_scopes: true,
prompt: 'consent',
code_challenge_method: CodeChallengeMethod.S256,
code_challenge: codes.codeChallenge
});
return { href, code_verifier: codes.codeVerifier };
}
async finishFlow(code: string, verifier: string): Promise<Tokens> {
const tokens = await oauth2.getToken({ code, codeVerifier: verifier })
return {
refresh_token: tokens.tokens.refresh_token!,
access_token: tokens.tokens.access_token!,
expires_in: tokens.tokens.expiry_date!,
token_type: 'Bearer',
scopes: tokens.tokens.scope!.split(' ')
};
}
"oauth2" is an instance of OAuth2Client from "google-auth-library". I initialize it here:
export const oauth2 = new google.auth.OAuth2({
clientId: YT_CLIENT_ID,
clientSecret: YT_CLIENT_SECRET,
redirectUri: `${APP_URI}/oauth`
});
Looking at the logs, the only out of the ordinary thing I notice is that the application/x-www-form-urlencoded body looks slightly different than the example https://developers.google.com/identity/protocols/oauth2/web-server#exchange-authorization-code
The POST request to "https://oauth2.googleapis.com/token" ends up looking like this:
code=4%2F0AX4XfWiKHVnsavUH7en0TywjPJVRyJ9aGN-JR8CAAcAG7dT-THxyWQNcxd769nzaHLUb8Q&client_id=XXXXXXXXXX-XXXXXXXXXXXXXXX.apps.googleusercontent.com&client_secret=XXXXXX-XXXXXXXXXXXXXXX-XX_XXX&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth&grant_type=authorization_code&code_verifier=KjOBmr4D9ISLPSE4claEBWr3UN-bKdPHZa8BBcQvcmajfr9RhWrgt7G429PLEpsP7oGzFGnBICu3HgWaHPsLhMkGBuQ2GmHHiB4OpY2F0rJ06wkpCjV2cCTDdpfRY~Ej
Notice that the "/" characters are not percent-encoded in the official example, but they are in my requests. Could this actually be the issue? I don't see how the official google auth library would have an issue this large.
If you check the documentation on expiration of refresh tokens you will see
A Google Cloud Platform project with an OAuth consent screen configured for an external user type and a publishing status of "Testing" is issued a refresh token expiring in 7 days.
Set your project to production mode and your refresh tokens will last longer then a week.
library
IMO if your refresh token expires your library should be requesting access again. However the library was created before the change that a refresh token gets revoked. This isnt a true refresh token expired error message and the library isn't detecting that.
A post over on the issue forum for your library may encourage them to patch it so that it is more obvious of an error message.
At any rate you need to delete the stored tokens for that user which will request authorization again.
update If its not that.
ensure that your computer / server has the correct time
revoke the users access directly via their google account. Under third party apps with access ensure that your app isnt listed if it is remove it.
Make sure that you are using the correct client type from google developer console.
Google node.js sample
var fs = require('fs');
var readline = require('readline');
var {google} = require('googleapis');
var OAuth2 = google.auth.OAuth2;
// If modifying these scopes, delete your previously saved credentials
// at ~/.credentials/youtube-nodejs-quickstart.json
var SCOPES = ['https://www.googleapis.com/auth/youtube.readonly'];
var TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
process.env.USERPROFILE) + '/.credentials/';
var TOKEN_PATH = TOKEN_DIR + 'youtube-nodejs-quickstart.json';
// Load client secrets from a local file.
fs.readFile('client_secret.json', function processClientSecrets(err, content) {
if (err) {
console.log('Error loading client secret file: ' + err);
return;
}
// Authorize a client with the loaded credentials, then call the YouTube API.
authorize(JSON.parse(content), getChannel);
});
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
*
* #param {Object} credentials The authorization client credentials.
* #param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
var clientSecret = credentials.installed.client_secret;
var clientId = credentials.installed.client_id;
var redirectUrl = credentials.installed.redirect_uris[0];
var oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, function(err, token) {
if (err) {
getNewToken(oauth2Client, callback);
} else {
oauth2Client.credentials = JSON.parse(token);
callback(oauth2Client);
}
});
}
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
*
* #param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
* #param {getEventsCallback} callback The callback to call with the authorized
* client.
*/
function getNewToken(oauth2Client, callback) {
var authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES
});
console.log('Authorize this app by visiting this url: ', authUrl);
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Enter the code from that page here: ', function(code) {
rl.close();
oauth2Client.getToken(code, function(err, token) {
if (err) {
console.log('Error while trying to retrieve access token', err);
return;
}
oauth2Client.credentials = token;
storeToken(token);
callback(oauth2Client);
});
});
}
/**
* Store token to disk be used in later program executions.
*
* #param {Object} token The token to store to disk.
*/
function storeToken(token) {
try {
fs.mkdirSync(TOKEN_DIR);
} catch (err) {
if (err.code != 'EEXIST') {
throw err;
}
}
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) throw err;
console.log('Token stored to ' + TOKEN_PATH);
});
}
/**
* Lists the names and IDs of up to 10 files.
*
* #param {google.auth.OAuth2} auth An authorized OAuth2 client.
*/
function getChannel(auth) {
var service = google.youtube('v3');
service.channels.list({
auth: auth,
part: 'snippet,contentDetails,statistics',
forUsername: 'GoogleDevelopers'
}, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
return;
}
var channels = response.data.items;
if (channels.length == 0) {
console.log('No channel found.');
} else {
console.log('This channel\'s ID is %s. Its title is \'%s\', and ' +
'it has %s views.',
channels[0].id,
channels[0].snippet.title,
channels[0].statistics.viewCount);
}
});
}
I've discovered my problem. Another part of my code was throwing an error while the authorization request was being sent (some weird async code). This caused the request to bail halfway through and send only a partial body. This resulted in the invalid_grant appearing in my logs which misled me to believe this was the root cause of my problem.
I fixed the other piece of my code and now the authorization request succeeds without issue.

How to get currently login user with passport-jwt

I implemented passport-jwt to authenticate user on protected route and also i want to check maybe the user login before creating first admin, please help me on how to do it.
this is my passport-jwt code that i have implemented
exports.getToken = function (user) {
return jwt.sign(user, config.secretKey, { expiresIn: 3600 });
};
var opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = config.secretKey;
exports.jwtPassport = passport.use(
new JwtStrategy(opts, (jwt_payload, done) => {
console.log("JWT payload: ", jwt_payload);
User.findOne({ _id: jwt_payload._id, }, (err, user) => {
if (err) {
return done(err, false);
} else if (user) {
return done(null, user);
} else {
return done(null, false);
}
});
})
);
If I understand your question correctly, you have authenticated a user and (s)he's logged in. Now, before creating an admin, you want to check if the currently logged in user hasn't expired or something else. Right ?
To do that:
You need to store JWT on the client-side so that whenever you call your API, you can attach the JWT in your request's authentication header. I say Authentication header because your ExtractJWT Strategy is fromAuthHeaderAsBearerToken.
With this you can attach your token to subsequent API calls headers. You also need to implement a middleware on your server-side so that the controller can verify whether the JWT in the Authorization header is valid or invalid.
Here is a good resource to understand the pipeline. Note that in this resource, they fromUrlQueryParameter as the extract strategy, but the concept is the same.

Auth0 login redirect to page keeps on loading when using Firefox as browser

I use an Auth0 login that handles the login for my platform after logging in it redirects you to the platform. With chrome, this works perfectly but in Firefox it keeps reloading the redirect page. I used the standard auth0 code to implement the logic into my page. I'm not sure what is going wrong and even where to start looking for a solution? Does anyone ever experienced the same bug and can help me fix the issue and also explain what is going wrong?
auth.ts file :
export class AuthService {
// Create an observable of Auth0 instance of client
auth0Client$ = (from(
createAuth0Client({
domain: authConfig.domain,
client_id: authConfig.clientId,
redirect_uri: `${window.location.origin}`,
audience: authConfig.audience,
})
) as Observable<Auth0Client>).pipe(
shareReplay(1), // Every subscription receives the same shared value
catchError(err => throwError(err))
);
// Manage Acces Token
// Observable method to retrieve access token and make it available for use in application
getTokenSilently$(options?): Observable<string> {
return this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.getTokenSilently(options)))
);
}
// Define observables for SDK methods that return promises by default
// For each Auth0 SDK method, first ensure the client instance is ready
// concatMap: Using the client instance, call SDK method; SDK returns a promise
// from: Convert that resulting promise into an observable
isAuthenticated$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.isAuthenticated())),
tap(res => (this.loggedIn = res))
);
handleRedirectCallback$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.handleRedirectCallback()))
);
// Create subject and public observable of user profile data
private userProfileSubject$ = new BehaviorSubject<any>(null);
userProfile$ = this.userProfileSubject$.asObservable();
// Create a local property for login status
loggedIn: boolean = null;
constructor(private router: Router) {
// On initial load, check authentication state with authorization server
// Set up local auth streams if user is already authenticated
this.localAuthSetup();
// Handle redirect from Auth0 login
this.handleAuthCallback();
}
// When calling, options can be passed if desired
// https://auth0.github.io/auth0-spa-js/classes/auth0client.html#getuser
getUser$(options?): Observable<any> {
return this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.getUser(options))),
tap(user => this.userProfileSubject$.next(user))
);
}
private localAuthSetup() {
// This should only be called on app initialization
// Set up local authentication streams
const checkAuth$ = this.isAuthenticated$.pipe(
concatMap((loggedIn: boolean) => {
if (loggedIn) {
// If authenticated, get user and set in app
// NOTE: you could pass options here if needed
return this.getUser$();
}
// If not authenticated, return stream that emits 'false'
return of(loggedIn);
})
);
checkAuth$.subscribe();
}
login(redirectPath: string = '/') {
// A desired redirect path can be passed to login method
// (e.g., from a route guard)
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) => {
// Call method to log in
client.loginWithRedirect({
redirect_uri: `${window.location.origin}`,
appState: { target: redirectPath },
});
});
}
private handleAuthCallback() {
// Call when app reloads after user logs in with Auth0
const params = window.location.search;
if (params.includes('code=') && params.includes('state=')) {
let targetRoute: string; // Path to redirect to after login processsed
const authComplete$ = this.handleRedirectCallback$.pipe(
// Have client, now call method to handle auth callback redirect
tap(cbRes => {
// Get and set target redirect route from callback results
targetRoute = cbRes.appState && cbRes.appState.target ? cbRes.appState.target : '/';
}),
concatMap(() => {
// Redirect callback complete; get user and login status
return combineLatest([this.getUser$(), this.isAuthenticated$]);
})
);
// Subscribe to authentication completion observable
// Response will be an array of user and login status
authComplete$.subscribe(([user, loggedIn]) => {
// Redirect to target route after callback processing
this.router.navigate([targetRoute]);
});
}
}
logout() {
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) => {
// Call method to log out
client.logout({
client_id: authConfig.clientId,
returnTo: window.location.origin,
});
});
}
}
auth-guard.ts
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean | UrlTree> | boolean {
return this.auth.isAuthenticated$.pipe(
tap(loggedIn => {
if (!loggedIn) {
this.auth.login(state.url);
}
})
);
}
}
I had this issue and found that it was because my browser was blocking third party cookies.
So my app was running on my own domain (or on localhost in dev mode), but I was using an auth0.com subdomain to log in with Auth0 and the browser was blocking cookies from auth0.com, which sent the auth0 JS in my app into an infinite loop of trying to check the logged in status, failing, refreshing the page, trying to check the logged in status, failing, refreshing the page, and so on.
It could be your browser's cookie settings or an ad blocking or privacy extension.

How to properly authenticate and use GoogleCalendar-API using oAuth2 in Vue CLI environment

I'm trying to integrate Google-Calendar-API in my Vue-CLI-based Webapp. I have decided to use the node.js version of GoogleAPI as learned from this site: https://developers.google.com/calendar/quickstart/nodejs#troubleshooting. However, I got:
TypeError: Expected input to be a Function or Object, got
undefined
This is for my personal project written in Vue-Cli, Vue Router, Vuetify.js, and additionally authenticated through Firebase (login via Google account). After a user logs in through Firebase UI (via Google account), they will get access to the dashboard page where they shall be able to access their calendar via Google's oAuth2 API system (stuck). I have initially tried using the browser-based javascript API but failed (and personally preferred node.js version later).
Dashboard.vue
<script>
import { config } from "#/../hidden/config.js";
const {google} = require('googleapis');
// If modifying these scopes, delete token.json.
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = '#/assets/token.json';
export default {
data() {
return {
oAuth2Client: null,
SCOPES: ['https://www.googleapis.com/auth/calendar.readonly'],
client_secret: "",
client_id: "",
redirect_uris: ""
};
},
methods: {
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
* #param {Object} credentials The authorization client credentials.
* #param {function} callback The callback to call with the authorized client.
*/
authorize: () => {
const self = this;
self.oAuth2Client = new google.auth.OAuth2(
client_id, client_secret, redirect_uris[0]);
self.getAccessToken(self.oAuth2Client);
self.oAuth2Client.setCredentials();
self.listEvents(oAuth2Client);
},
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
* #param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
* #param {getEventsCallback} callback The callback for the authorized client.
*/
getAccessToken: () => {
const self = this;
const authUrl = self.oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
console.log('Authorize this app by visiting this url:', authUrl);
self.oAuth2Client.getToken(code, (err, token) => {
if (err) return console.error('Error retrieving access token', err);
self.oAuth2Client.setCredentials(token);
// self.listEvents();
});
}
/**
* Lists the next 10 events on the user's primary calendar.
* #param {google.auth.OAuth2} auth An authorized OAuth2 client.
*/
/*
listEvents: () => {
const self = this;
const auth = self.oAuth2Client;
const calendar = google.calendar({version: 'v3', auth});
calendar.events.list({
calendarId: 'primary',
timeMin: (new Date()).toISOString(),
maxResults: 10,
singleEvents: true,
orderBy: 'startTime',
}, (err, res) => {
if (err) return console.log('The API returned an error: ' + err);
const events = res.data.items;
if (events.length) {
console.log('Upcoming 10 events:');
events.map((event, i) => {
const start = event.start.dateTime || event.start.date;
console.log(`${start} - ${event.summary}`);
});
} else {
console.log('No upcoming events found.');
}
});
}
*/
},
created: function() {
const {credentials} = require("#/credentials.json");
this.client_secret = credentials.installed.client_secret;
this.client_id = credentials.installed.client_id;
this.redirect_uris = credentials.installed.redirect_uris;
//this.authorize();
}
};
</script>
I expect to be able to connect to the Google Calendar API and start working on actually manipulating the calendar event info for my purpose. However, I'm getting the error:
TypeError: Expected input to be a Function or Object, got
undefined
.
I have tried looking for people with similar problems online, however I didn't find any video guides or written guides for Vue-cli projects that use Google APIs.
I do confess that I have modified my code a little bit from the referenced sample from the website to avoid using fs and readline npm packages. However, the error message was same in those cases too.