Is this the correct and secure way to connect Next.js + NextAuth with a Django Rest Framework API? - authentication

I have been working on a Next.js app with a custom backend using Django Rest Framework, with the main focus on authentication with social platforms (Google, Github etc). Here's the flow I am wanting to use:
Have the NextAuth do the heavy lifting for social authentication. It gets back, for instance, an access token and an id token when the user wants to login with his/her Google account.
Put the id token and access token given back by Google into the NextAuth session object.
In the frontend, use those two tokens in the session object to make a POST request to the DRF backend which essentially accepts the access token and id token and returns a access token and a refresh token. NB. The DRF backend has dj-rest-auth and django-allauth setup to handle social authentication.
The DRF backend sends back the tokens in the form of HTTPOnly cookies. So, next time I want to make a request to the DRF API, the cookies should be passed along the request.
Is this correct and secure, or am I shooting myself in the foot?
My code for context:
index.tsx
import React, { useEffect } from "react";
import { signIn, signOut, useSession } from "next-auth/client";
import { Typography, Button, Box } from "#material-ui/core";
import { makeUrl, BASE_URL, SOCIAL_LOGIN_ENDPOINT } from "../urls";
import axios from "axios";
axios.defaults.withCredentials = true;
function auth() {
const [session, loading] = useSession();
useEffect(() => {
const getTokenFromServer = async () => {
// TODO: handle error when the access token expires
const response = await axios.post(
// DRF backend endpoint, api/social/google/ for example
// this returns accessToken and refresh_token in the form of HTTPOnly cookies
makeUrl(BASE_URL, SOCIAL_LOGIN_ENDPOINT, session.provider),
{
access_token: session.accessToken,
id_token: session.idToken,
},
);
};
if (session) {
getTokenFromServer();
}
}, [session]);
return (
<React.Fragment>
<Box
display="flex"
justifyContent="center"
alignItems="center"
m={5}
p={5}
flexDirection="column"
>
{!loading && !session && (
<React.Fragment>
<Typography variant="button">Not logged in</Typography>
<Button
variant="outlined"
color="secondary"
onClick={() => signIn()}
>
Login
</Button>
</React.Fragment>
)}
{!loading && session && (
<React.Fragment>
<Typography>Logged in as {session.user.email}</Typography>
<pre>{JSON.stringify(session, null, 2)}</pre>
<Button
variant="outlined"
color="primary"
onClick={() => signOut()}
>
Sign Out
</Button>
</React.Fragment>
)}
</Box>
</React.Fragment>
);
}
export default auth;
api/auth/[...nextauth].ts
import NextAuth from "next-auth";
import { InitOptions } from "next-auth";
import Providers from "next-auth/providers";
import { NextApiRequest, NextApiResponse } from "next";
import axios from "axios";
import { BASE_URL, SOCIAL_LOGIN_ENDPOINT, makeUrl } from "../../../urls";
import { AuthenticatedUser, CustomSessionObject } from "../../../types";
import { GenericObject } from "next-auth/_utils";
const settings: InitOptions = {
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorizationUrl:
"https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code",
}),
],
secret: process.env.NEXT_AUTH_SECRET,
session: {
maxAge: 6 * 60 * 60, // 6 hours
},
callbacks: {
async signIn(user: AuthenticatedUser, account, profile) {
if (account.provider === "google") {
const { accessToken, idToken, provider } = account;
user.accessToken = accessToken;
user.idToken = idToken;
user.provider = provider;
return true;
}
return false;
},
async session(session: CustomSessionObject, user: AuthenticatedUser) {
session.accessToken = user.accessToken;
session.idToken = user.idToken;
session.provider = user.provider;
return session;
},
async jwt(token, user: AuthenticatedUser, account, profile, isNewUser) {
if (user) {
token.accessToken = user.accessToken;
token.idToken = user.idToken;
token.provider = user.provider;
}
return token;
},
},
};
export default (req: NextApiRequest, res: NextApiResponse) => {
return NextAuth(req, res, settings);
};
I am stressed out by the fact that whether the session object is secure enough to store the tokens. I also want to implement a mechanism to refresh the access tokens using the refresh token when the access token expires.

I ended up solving this issue over time. I wrote a two-part article outlining how I solved it, which can be found here and here

Related

How to consume Next.JS Rest Endpoints secured with Amplify from a React Native app

Background:
My current stack is a Next server to use as an admin portal and REST API for a Mobile App running with Expo - React Native. The Next Server is currently hosted as a Lambda#Edge.
I have secured both the Next server and the React Native app with AWS Amplify's withAuthenticator wrapper. (I also tried specific auth packages like Next-Auth and Expo's auth package)
Problem:
However, I can't figure out how to add the Auth info (Access_token) to my REST Requests from Mobile app -> Next Server
I tried adding the tokens as bearer headers to the API without luck after that I was fairly sure it all has to be set up and sent via cookies.
BUT I am stuck on how to actually implement these cookies properly. I was hoping the endpoints:[] config could be used to set up my own domain to post to and handle the cookies. Reading the request on the server showed that it contained no Auth info when posted with this method.
Likewise using RTK Query (Preferably I add all the Auth to this instead of Amplify's API setup) I don't have the correct info to make an Authorized api request
Here are some snippets of the working page Authentication for both apps
API Endpoint /api/version:
import type { NextApiRequest, NextApiResponse } from 'next'
import { withSSRContext } from 'aws-amplify'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data | Error>,
) {
const { Auth } = withSSRContext({req})
try {
const user = await Auth.currentAuthenticatedUser()
return res.status(200).json({
version: '1.0.0',
user: user.username,
})
} catch (err) {
console.log(err)
return res.status(200).json({
message: 'Unauthenticated',
})
}
}
Mobile App Config:
import {
useAuthenticator,
withAuthenticator,
} from '#aws-amplify/ui-react-native'
import { Amplify, Auth } from 'aws-amplify'
import awsconfig from './aws-exports'
Amplify.configure({
...awsconfig,
API: {
endpoints: [
{
name: 'MyApi',
endpoint: 'http://NextIP:NextPort/api/',
},
],
},
})
Auth.configure(awsconfig)
export default withAuthenticator(App)
Mobile Screen:
import { API } from 'aws-amplify'
function getData() {
const apiName = 'MyApi'
const path = '/version'
const myInit = {
headers: {}, // OPTIONAL
}
return API.get(apiName, path, myInit)
}
export default function ModalScreen() {
// Get token / Cookie for auth
// const { data, isLoading, error } = useGetApiVersionQuery(null) // RTK Query
getData() // Amplify
.then(response => {
console.log(response)
})
.catch(error => {
console.log(error.response)
})
return ( <></>)}
I found a solution, however, could not get the Next-Auth middleware to fire when the token was sent using the Bearer token in headers. Which is my ideal way of handling the routes.
I wrapped the getToken({req}) call so that if there is no JWT Web token it would try encode the token separately
Lastly ChatGpt somehow got me onto the package aws-jwt-verify which has everything you need to verify a token generated by aws-amplify/auth, in my case from react-native.
components/utils/auth.utils.ts
import { NextApiRequest } from 'next'
import { CognitoJwtVerifier } from 'aws-jwt-verify'
import { getToken } from 'next-auth/jwt'
// Verifier that expects valid token:
const verifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USERPOOL_ID ?? '',
tokenUse: 'id',
clientId: process.env.COGNITO_CLIENT_ID ?? '',
issuer: process.env.COGNITO_ISSUER ?? '',
})
export async function getMobileToken(req: NextApiRequest) {
let token = null
try {
token = await getToken({ req })
} catch (error) {
console.log('Could not get JWT Web Token')
}
try {
if (!token)
token = await getToken({
req,
async decode({ token }) {
if (!token) return null
const decoded = await verifier.verify(token)
return decoded
},
})
} catch (error) {
return null
}
console.log('Mobile Token:', token)
return token
}

Auth token not reloaded

I have a problem with an authToken in my Vue 3 application (very beginner in JS and Vue).
When the user is authenticated (after a post /login), the app receives a token which is stored in the local storage (I know it is perhaps not a good idea, it will be another question later).
With this token, the app can make some GETs to the endpoint. All is working fine.
Of course, the user can logout. In this case I remove the token from the local storage and the user is redirected to the login page.
The problem is now : when the user reconnects again, a new token is received and stored. But all the GET requests are made with the previous token. So of course all the requests are refused (the previous token is no more on the server).
When the app receives the token :
export function loginUser(email, password) {
return new Promise(async (resolve, reject) => {
try {
let res = await axios({
url: `${REST_ENDPOINT}/login`,
method: 'POST',
data: {
email: email,
password: password
}
})
setAuthToken(res.data)
resolve()
}
catch (err) {
console.error('Caught an error during login:', err.request.response)
reject(JSON.parse(err.request.response))
}
})
}
export function logoutUser() {
clearAuthToken()
}
export function setAuthToken(token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token.token}`
console.log('je suis dans le setAUthToken')
localStorage.setItem(AUTH_TOKEN_KEY, token.token)
}
And for the requests, I created a repository system. So in my component I just have :
import { RepositoryFactory } from "../../repositories/RepositoryFactory";
const OrganizationsRepository = RepositoryFactory.get("organizations");
export default {
name: "About",
data() {
return {
isLoading: false,
organizations: [],
page: 1,
filterByName: '',
filterByStateId: 'VALIDATED',
};
},
created() {
this.page = 1
this.getOrganizations();
},
methods: {
async getOrganizations() {
this.isLoading = true;
console.log('avant le get dans la view')
const { data } = await OrganizationsRepository.getOrganizations(this.buildParams());
this.isLoading = false;
this.organizations = data;
},
I am sure (but to confirm) that it is normal because when the app is loading, the token is stored and no more changed (thanks to the import just below).
So in this case, how to force the component to reload the token for each request ?

Cognito '/oauth2/token' end point not returning 'id_token' for Authorization Code Grant with PKCE

Cognito '/oauth2/token' end point not returning 'id_token' for Authorization Code Grant with PKCE even though the documentation says it will be returned (Link). It should return the id_token as well. Is this normal or I need to configure more? I have added the content of the git issue opened by me below if this is helpful(Issue 7393)
To Reproduce
Steps to reproduce the behavior:
Configure the user pool with hosted UI support
Go to hosted UI and complete the login
User will be redirected to the "redirectSignIn" URL
Hub.listen('auth') event fired with error message "Sign in failure Error: Username and Pool information are required."
Check the session for ID token
Check the code challenge request to get the tokens(/oauth2/token request)
Both do not have the ID token. /oauth2/token only returns access_token, expires_in, refresh_token and token_type
Expected behavior
It should also return id_token
Code Snippet
import React, {useEffect, useState} from 'react';
import { Amplify, Auth, Hub } from 'aws-amplify';
import {AmplifyConfig} from '../../config/amplifyConfig';
Amplify.configure({
Auth: AmplifyConfig.auth
});
const AuthorizePage = (props: any) => {
const [user, setUser] = useState(null);
useEffect(() => {
Hub.listen('auth', ({ payload: { event, data } }) => {
switch (event) {
case 'signIn':
case 'cognitoHostedUI':
getUser().then(userData => setUser(userData));
break;
case 'signOut':
setUser(null);
break;
case 'signIn_failure':
case 'cognitoHostedUI_failure':
console.log('Sign in failure', data);
break;
}
});
getUser().then(userData => setUser(userData));
}, []);
function getUser() {
return Auth.currentAuthenticatedUser()
.then(userData => userData)
.catch(() => console.log('Not signed in'));
}
return (
<div className="menu-card-filter--items" data-id="aperitif">
<span>
Authorizing
</span>
</div>
)
}
export default AuthorizePage;
Screenshots
Request
https://user-images.githubusercontent.com/12485276/101932415-bccab580-3c00-11eb-8cde-222d72f0d956.png
Response
https://user-images.githubusercontent.com/12485276/101932467-d3710c80-3c00-11eb-9d3b-778faee43fa4.png
What is Configured?
Auth: {
mandatorySignIn: true,
region: "******-*",
userPoolId: "**-******-*_*******",
userPoolWebClientId: "**********************",
oauth: {
domain: "**********************.amazoncognito.com",
scope: [
"phone",
"email",
"profile",
],
redirectSignIn: "http://localhost:3000/authorize",
redirectSignOut: "http://localhost:3000/logout",
responseType: "code"
}
}
Add the 'openid' scope to the scope list in your auth configuration.
The openid scope returns all user attributes in the ID token that are readable by the client. The ID token is not returned if the openid scope is not requested by the client.
https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.htmlhere

How to get FB Access Token with Expo

I'm building app where i need to make Facebook Graph API requests in many places. But i dont know how to retrieve access token and then make Graph API request.
I'm using Expo, React Native and Firebase. I would like to do it without installing Xcode and/or Android Studio.
Login is working fine. My code is below:
async loginWithFacebook() {
try {
const {
type,
token,
expires,
permissions,
declinedPermissions,
} = await Expo.Facebook.logInWithReadPermissionsAsync('<APP_ID', {
permissions: ['email', 'public_profile'],
});
if (type === 'success') {
const credential = f.auth.FacebookAuthProvider.credential(token)
f.auth().signInAndRetrieveDataWithCredential(credential).catch((error) => {
console.log(error)
})
var that = this;
const response = await fetch(`https://graph.facebook.com/me/?fields=id,name&access_token=${token}`);
const userInfo = await response.json();
this.setState({userInfo});
this.setState({
dataSource: userInfo.data,
isLoading: false,
});
} else {
// type === 'cancel'
}
} catch ({ message }) {
alert(`Facebook Login Error: ${message}`);
}
}
Can someone help me and give me some tips how i can use access token everywhere in my app?
Thank you in advance
Getting the token and saving it into AsyncStorage
Well the code that you have written is basically correct. You have successfully got the access token. It comes back to you when you make the Expo.Facebook.logInWithReadPermissionsAsync request. Once you have it you could then store it in Redux or AsyncStorage to be used at a later date.
logIn = async () => {
let appID = '123456789012345' // <- you'll need to add your own appID here
try {
const {
type,
token, // <- this is your access token
expires,
permissions,
declinedPermissions,
} = await Expo.Facebook.logInWithReadPermissionsAsync(appID, { permissions: ['public_profile', 'email'], });
if (type === 'success') {
// Get the user's name using Facebook's Graph API
const response = await fetch(`https://graph.facebook.com/me/?fields=id,name&access_token=${token}`); //<- use the token you got in your request
const userInfo = await response.json();
alert(userInfo.name);
// you could now save the token in AsyncStorage, Redux or leave it in state
await AsyncStorage.setItem('token', token); // <- save the token in AsyncStorage for later use
} else {
// type === 'cancel'
}
} catch ({ message }) {
alert(`Facebook Login Error: ${message}`);
}
}
app.json
Also remember to add the following to your app.json, obviously replacing the values with your own. You get these by registering your app with Facebook, you can see more about that here https://docs.expo.io/versions/latest/sdk/facebook/#registering-your-app-with-facebook
{
"expo": {
"facebookScheme": "fb123456789012345",
"facebookAppId": "123456789012345", // <- this is the same appID that you require above when making your initial request.
"facebookDisplayName": "you_re_facebook_app_name",
...
}
}
Getting token from AsyncStorage
Then if you wanted to make another request at a later time you could have a function similar to this where you get the token out of AsyncStorage and then use it to make your request.
makeGraphRequest = async () => {
try {
let token = await AsyncStorage.getItem('token'); // <- get the token from AsyncStorage
const response = await fetch(`https://graph.facebook.com/me/?fields=id,name&access_token=${token}`); // <- use the token for making the graphQL request
const userInfo = await response.json();
alert(userInfo.name)
} catch (err) {
alert(err.message)
}
}
Snack
I would make a snack to show you this working however, snacks do not allow editing of the app.json file (as far as I can tell). So here is something that you could replace your App.js with and then if you added your appIDs etc to the app.json it should work.
import React from 'react';
import { AsyncStorage, Text, View, StyleSheet, SafeAreaView, Button } from 'react-native';
export default class App extends React.Component {
logIn = async () => {
let appID = '123456789012345' // <- you'll need to add your own appID here
try {
const {
type,
token, // <- this is your access token
expires,
permissions,
declinedPermissions,
} = await Expo.Facebook.logInWithReadPermissionsAsync(appID, {
permissions: ['public_profile', 'email'],
});
if (type === 'success') {
// Get the user's name using Facebook's Graph API
const response = await fetch(`https://graph.facebook.com/me/?fields=id,name&access_token=${token}`); //<- use the token you got in your request
const userInfo = await response.json();
console.log(userInfo);
alert(userInfo.name);
// you could now save the token in AsyncStorage, Redux or leave it in state
await AsyncStorage.setItem('token', token); // <- save the token in AsyncStorage for later use
} else {
// type === 'cancel'
}
} catch ({ message }) {
alert(`Facebook Login Error: ${message}`);
}
}
makeGraphRequest = async () => {
try {
let token = await AsyncStorage.getItem('token');
// Get the user's name using Facebook's Graph API
const response = await fetch(`https://graph.facebook.com/me/?fields=id,name&access_token=${token}`);
const userInfo = await response.json();
alert(userInfo.name)
} catch (err) {
alert(err.message)
}
}
render() {
return (
<View style={styles.container}>
<Button title={'Sign in to Facebook'} onPress={this.logIn} />
<Button title={'Make GraphQL Request'} onPress={this.makeGraphRequest} />
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white'
}
});

Getting invalid token from Auth0 in my new Expo app

I’m implementing Auth0 authentication on a new Expo app following this example:
https://github.com/expo/auth0-example
It seems to make a call to Auth0 and successfully obtain a token but immediately after logging the response in the console, it also gives me the following error:
Possible Unhandled Promise Rejection (id: 0) [InvalidTokenError:
Invalid token specified: Unexpected token V in JSON at position 0]
The response I get is this:
params:
access_token: “Vku7HOclH7pVi52bmzGHga89VwpfK_Y4”
exp://10.0.0.215:19000/–/expo-auth-session: “”
expires_in: “7200”
scope: “openid profile”
token_type: “Bearer”
proto: Object
type: “success”
url: “exp://10.0.0.215:19000/–/expo-auth-session#access_token=Vku7HOclH7pVi52bmzGHga89VwpfK_Y4&scope=openid%20profile&expires_in=7200&token_type=Bearer”
When I check the access_token on jwt.io, it indicates an invalid signature. Any idea what may be the issue here?
Here’s my full code:
import React, { Component } from 'react';
import { AuthSession } from 'expo';
import { Alert, Button, View, Text } from 'react-native';
import jwtDecoder from 'jwt-decode';
import styles from '../constants/styles';
const auth0ClientId = 'my_client_id_for_my_mobile_app_from_Auth0_dashboard';
const auth0Domain = 'https://mydomain.auth0.com';
/**
* Converts an object to a query string.
*/
function toQueryString(params) {
return '?' + Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
}
export default class Login extends Component {
constructor(props) {
super(props);
this.state = {
username: null
};
}
_loginWithAuth0 = async () => {
const redirectUrl = AuthSession.getRedirectUrl();
console.log(`Redirect URL (add this to Auth0): ${redirectUrl}`);
const result = await AuthSession.startAsync({
authUrl: `${auth0Domain}/authorize` + toQueryString({
client_id: auth0ClientId,
response_type: 'token',
scope: 'openid profile',
redirect_uri: redirectUrl,
}),
});
console.log(result);
if (result.type === 'success') {
this.handleParams(result.params);
}
}
handleParams = (responseObj) => {
if (responseObj.error) {
Alert.alert('Error', responseObj.error_description
|| 'something went wrong while logging in');
return;
}
const encodedToken = responseObj.access_token;
const decodedToken = jwtDecoder(encodedToken, { header: true });
const username = decodedToken.name;
debugger;
this.setState({ username });
}
render() {
return (
<View style={styles.welcomeScreen}>
<Text>Welcome to My Expo App!</Text>
<Button title="Login with Auth0" onPress={this._loginWithAuth0} />
</View>
);
}
}
P.S. My Expo app is using SDK version 31.0.0
The Access Token for non-Custom APIs are in opaque (Similar to the token you have received) and not a JWT. This is because you have not set an audience in the Authorization URL. Auth0 will only give JWT Access Tokens for Custom APIs.
The ID Token you received will be in JWT format, since you requested for openid scope.