Getting invalid token from Auth0 in my new Expo app - react-native

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.

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
}

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

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

How can i navigate to homescreen after facebook authentication using firebase + expo

Hello I'm new to react native and I'm trying to navigate to Home Screen with a successful authentication, but this generates an error, can someone help me? please
loginWithFacebook = async() => {
await Facebook.initializeAsync(
'235487284376992',
);
const { type, token } = await Facebook.logInWithReadPermissionsAsync(
{ permissions: ['public_profile'] }
);
if (type === 'success') {
const credential = firebase.auth.FacebookAuthProvider.credential(token);
this.props.navigation.navigate('Home')
firebase
.auth().signInWithCredential(credential).catch(error => {
console.log(error);
});
}
Firebase is working, however an error is generated during navigation
The error when try login
this error only happens after the first authentication, then the navigation normally occurs
Are you using react-native-firebase?
Which version are you using?
If you are using react-native-firebase v6+ , you need to install #react-native-firebase/auth module.
React Native Firebase version 6 has been re-created from the ground up.
And also, you must import the firebase from auth module on header
import {firebase} from "#react-native-firebase/auth";
This is my code.
import {
createTypes,
createAction,
transformNetworkError,
socialLoginError,
} from '../../utils/actions';
import {getWithData} from '../../utils/request';
import {OneSignalUtils} from '../../utils/oneSignalUtils';
import {FirebaseUtils} from '../../utils/firebaseServices';
import { LoginManager, AccessToken } from 'react-native-fbsdk';
import { GoogleSignin } from '#react-native-community/google-signin';
import {firebase} from "#react-native-firebase/auth";
const facebookLogin = () => async dispatch => {
const loginAction = {
do: () => createAction(LOGIN.DO, {}),
success: (response, authData) => createAction(LOGIN.SUCCESS, {userData: response, authData: authData}),
failed: (error) => createAction(LOGIN.FAILED, error),
};
try {
dispatch(loginAction.do());
const result = await LoginManager.logInWithPermissions(['public_profile', 'email']);
if (result.isCancelled) {
dispatch(loginAction.failed(socialLoginError({status: true, message: 'Facebook login canceled.'})));
}
const logindata = await AccessToken.getCurrentAccessToken();
if (!logindata) {
dispatch(loginAction.failed(socialLoginError({status: true, message: 'Something went wrong obtaining access token.'})));
return null;
} else {
const credential = firebase.auth.FacebookAuthProvider.credential(logindata.accessToken);
const firebaseUserCredential = await firebase.auth().signInWithCredential(credential);
const data = firebaseUserCredential.user.toJSON();
await FirebaseUtils.userRef.doc(data.uid).set(data);
dispatch(loginAction.success());
return data;
}
} catch (e) {
console.log('facebook login error', e);
dispatch(loginAction.failed(socialLoginError({status: true, message: 'Something went wrong.'})));
return null;
}
};

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'
}
});

Token not being added to local storage after successful request

I'm having trouble with the aurelia-auth plugin. I can request a token from my back-end and get a successful response - however, my app doesn't appear to be authenticated and no token is saved to local storage.
auth-config.js
var config = {
baseUrl: 'http://localhost:64794',
signupUrl: 'users',
loginUrl: 'api/values/PostPassword',
tokenName: 'id_token',
loginRedirect: '#/welcome',
authHeader: 'Authorization',
authToken: 'Bearer',
storage: 'localStorage'
}
export default config;
login.js
import {AuthService} from 'aurelia-auth';
import {inject} from 'aurelia-framework';
#inject(AuthService)
export class Login {
heading = 'Login';
email = '';
password = '';
loginError = '';
constructor(auth) {
this.auth = auth;
};
login() {
return this.auth.login(this.email, this.password)
.then(response => {
console.log("Login response: " + response);
console.log("Auth: " + this.auth.isAuthenticated());
})
.catch(error => {
this.loginError = error.response;
});
};
}
Console output:
Login response: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImIiLCJuYmYiOjE1MDkyNzkxMzYsImV4cCI6MTUwOTI4MDMzNiwiaWF0IjoxNTA5Mjc5MTM2fQ.4QZu8pQI-K_71x_CKT9ANu1vQD7VvVUcyep51CvvCXg
login.js:27 Auth: false
Any advice would be appreciated.
token (or the object containing it) is expected to be the value of prop 'access_token' in the response by default in aurelia-auth
so you need to change the structure of your api response to
{ access_token: YOUR_TOKEN }
if you have no control over the server, you could still extract the token by overriding the default config. one in question here is responseTokenProp, more here
also, for a clearer picture as to how the token is extracted, have a look here