I am using django-rest-framework-jwt and react-redux for my SPA.
Need refresh token that expires in 5 minutes.
Refresh works during the 5 minutes.
After it does not work, console show this error:
POST http://localhost:8000/auth/api-token-refresh/ 400 (Bad Request)
createError.js:17 Uncaught (in promise) Error: Request failed with status code 400
at createError (createError.js:17)
at settle (settle.js:19)
at XMLHttpRequest.handleLoad (xhr.js:78)
and postman show this:
{
"non_field_errors": [
"Signature has expired."
]
}
there's the middleware's code
import axios from "axios";
import * as urls from "../helpers/url";
import { authUpdateToken } from "../actions/auth";
const jwthunk = ({ dispatch, getState }: any) => (next: any) => (action: any) => {
if (typeof action === 'function') {
if (getState().auth && getState().auth.token) {
const currentToken = getState().auth.token;
verifyToken(currentToken)
.then((tokenVerified: any) => {
refreshToken(tokenVerified, dispatch)
})
.catch(() => {
refreshToken(currentToken, dispatch)
})
} else {
console.log('Not Auth');
}
}
return next(action);
}
export default jwthunk;
const verifyToken = async (token: any) => {
const body = { token };
let verifiedToken = '';
await axios.post('http://localhost:8000/auth/api-token-verify/', body)
.then(({ data: { code, expires, token } }: any) => {
verifiedToken = token;
});
return verifiedToken;
}
const refreshToken = async (token: any, dispatch: any) => {
const body = { token }
await axios.post('http://localhost:8000/auth/api-token-refresh/', body)
.then((response: any) => {
dispatch(authUpdateToken({ token }));
})
}
django-rest-framework-jwt send an unique token, without refresh-token
I assume that there library have a flaw.
You cant refresh expired token..
Solution
1) Do a monkey patch in your code (Check the below commit code)
https://github.com/jpadilla/django-rest-framework-jwt/pull/348
2)Switch to different library
3)you need to run a timer in you application(front end) and request for access token every 5 minutes before expiry. (which is not ideal way)
Related
I am trying to cover my API, protected by Auth0, with unit tests.
Wrote the below:
'use strict';
const createJWKSMock = require('mock-jwks').default;
const startAuthServer = jwksServer => {
const jwks = createJWKSMock(jwksServer);
jwks.start();
return jwks;
};
const getToken = jwks => {
const token = jwks.token({
iss: `https://${process.env.AUTH0_DOMAIN}/`,
sub: 'testprovider|12345678',
aud: [
`${process.env.AUTH0_AUDIENCE}`,
`https://${process.env.AUTH0_DOMAIN}/userinfo`
],
iat: 1635891021,
exp: 1635977421,
azp: 'AndI...3oF',
scope: 'openid profile email'
});
return token;
};
const stopAuthServer = jwks => {
jwks.stop();
};
describe('/promoter/event/:id', () => {
let server, token, jwks;
beforeAll(() => {});
beforeEach(async () => {
jest.clearAllMocks();
jwks = startAuthServer(`https://${process.env.AUTH0_DOMAIN}`);
token = getToken(jwks);
server = require('../../../');
await server.ready();
});
afterEach(async () => {
stopAuthServer(jwks);
});
it('GET for a non-exising event returns 404', async () => {
const mockSelect = jest.fn();
mockSelect
.mockResolvedValueOnce({
rowCount: 1,
rows: [{ row_to_json: { id: 1 } }]
})
.mockResolvedValueOnce({
rowCount: 0,
rows: []
});
server.pg.query = mockSelect;
// const token = `eyJhb...u5WYA`;
const response = await server.inject({
method: 'GET',
url: '/promoter/event/25',
headers: { Authorization: `Bearer ${token}` }
});
expect(response.statusCode).toEqual(404);
});
});
If I run the code with a token I generate with getToken the Auth0 plugin does not let the token pass, and I am getting 500.
If I use/uncomment the token returned by Auth0 the tests pass. Hence, it is pretty clear that the problem is the token.
I decoded both tokens - those issued by Auth0 and those made by mock-jwks and the only difference I noticed is the header in the tokens made by mock-jwks is missing typ property.
The ones made by Auth0 look like:
{
"alg": "RS256",
"typ": "JWT",
"kid": "Mk...OQ"
}
While those produced with mock-jwks look like:
{
"alg": "RS256",
"kid": "Mk...OQ"
}
Internally, my server is using fastify-auth0-verify to verify the tokens.
I have also tried mocking the auth server with nock as below:
nock(`https://${process.env.AUTH0_DOMAIN}`)
.get('/.well-known/jwks.json')
.reply(200, nockReply);
It is not working either. The call never gets to it, and further, NodeJS prints a warning:
Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
If I disable mock-jwks, Jest/tests exit just fine both with nock and without nock.
Suggestions?
I am currently refreshing the id token with Amplify in my serverless backend.
What I do is I send the refreshToken request to backend, and use cognitoUser.refreshSession to get the new token.
Here is my backend code:
import Amplify from 'aws-amplify';
import { awsconfig } from '../aws-exports';
class CognitoService {
constructor() {
Amplify.configure(awsconfig);
this.auth = Amplify.Auth;
}
async refreshToken() {
try {
const cognitoUser = await this.auth.currentAuthenticatedUser();
const cognitoSession = await this.getCognitoSession(cognitoUser);
const refreshedSessionToken = await this.refreshSession(cognitoUser, cognitoSession);
return refreshedSessionToken;
} catch (error) {
console.log(error);
throw new Error('Fail to refresh token');
}
}
getToken(userSession) {
return {
accessToken: userSession.idToken.jwtToken,
refreshToken: userSession.refreshToken.token,
expire: userSession.idToken.payload.exp
};
}
getCognitoSession(cognitoUser) {
return new Promise((resolve, reject) => {
cognitoUser.getSession((err, result) => {
if (err || !result) {
reject(new Error('Failure getting Cognito session: ' + err));
return
}
console.debug('Successfully got session: ' + JSON.stringify(result));
resolve(result);
})
})
}
refreshSession(cognitoUser, cognitoSession) {
return new Promise((resolve, reject) => {
cognitoUser.refreshSession(cognitoSession.getRefreshToken(), (err, session) => {
if (err || !session) {
reject(new Error('Failure refresh Cognito session: ' + err));
return
}
resolve(this.getToken(session));
})
})
}
}
export { CognitoService };
Actually it works well in most of the time. The token is updated in frontend every single successful fetch.
However after few times it will be failed.
Request fail after few successful request
The refresh token is valid for 30days and the idtoken is valid for 1hour.
The error message I got:
INFO The user is not authenticated
ERROR Error: Fail to refresh token at cognitoService_CognitoService.refreshToken
Any idea about this? Thanks.
So I have this react native code that sends a token in string format, yes I've checked that var token = getAccessToken() is a string and I've console.log it to ensure it is a JWT token as well. But on the Django side when I check request.headers.get('Authorization', None) it outputs: 'Bearer [object Object]' what's going on?
React Native Code
import {Auth} from 'aws-amplify';
export async function getAccessToken() {
try {
const currentUser = await Auth.currentAuthenticatedUser();
console.log(currentUser);
Auth
.currentSession()
.then(res => {
let accessToken = res.getAccessToken();
// let jwt = accessToken.getJwtToken();
// You can print them to see the full objects
// console.log(`myAccessToken: ${JSON.stringify(accessToken)}`);
// console.log(`myJwt: ${JSON.stringify(jwt)}`);
console.log(accessToken.jwtToken)
return accessToken.jwtToken
});
} catch (error) {
console.log('error signing up:', error);
}
}
const getPosts = () => {
var token = getAccessToken();
const config = {
headers: { Authorization: `Bearer ` + token }
};
axios
.get(`${url}/posts`, config)
.then(response => {
console.log(response)
setData(response.data);
})
.catch(error => {
console.log(JSON.stringify(error));
});
}
I also tried
const config = {
headers: { Authorization: `Bearer ${token}` }
};
I also tried
function getPosts() {
var token = getAccessToken().then(token => {
const config = {
headers: {
Authorization: `Bearer ${token}`
}
};
console.log(token)
axios
.get(`${url}/posts`, config)
.then(response => {
console.log(response)
setData(response.data);
})
.catch(error => {
console.log(JSON.stringify(error));
});
}).catch(error => {
console.log(JSON.stringify(error));
});;
};
and console.log(token) is outputting "undefined"
Update getAccessToken to return result of
Auth .currentSession()
And
Make getPosts function async and await getAccessToken().
OR
Use the then block to result of promise
getAccessToken().then(token=>{ // Call the api },err=>{ // Handle the error }
Otherwise what you are getting is a promise that's not resolved yet.
Hi and thanks in advance,
I've successfully setup JWT authentication using django-rest-framework-simplejwt and React but I'm still very confused about the advantages and specifically database hits.
I'm using simplejwt with ROTATE_REFRESH_TOKENS': True 'BLACKLIST_AFTER_ROTATION': True, when my access_token expire I ask for a new one through /api/token/refresh and it blacklist old tokens, I'm using axios interceptors to perform that automatically.
But in my understanding the benefits of JWt is that they are stateless, meaning I don't have to hit the user database table everytime I want to make an a request that needs authentication permission.
The problem is even with a simple view like this :
class IsConnecteddAPI(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, *args, **kwargs):
data = "You seem to be connected"
return Response(data, status=status.HTTP_200_OK)
using django-silk I see that it still performs 1 query to my user table when I call it with a valid access token, is that normal ? If so why do we say that JWT are stateless ? I'm really confused.
That's my axios code if needed :
import axios from "axios";
const baseURL = "http://localhost:5000";
const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
Authorization: localStorage.getItem("accesstoken")
? "JWT " + localStorage.getItem("accesstoken")
: null,
"Content-Type": "application/json",
accept: "application/json",
},
});
const axioAnonymousInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
"Content-Type": "application/json",
accept: "application/json",
},
});
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const originalRequest = error.config;
if (typeof error.response === "undefined") {
alert(
"A server/network error occurred. " +
"Looks like CORS might be the problem. " +
"Sorry about this - we will get it fixed shortly."
);
return Promise.reject(error);
}
if (
error.response.status === 401 &&
originalRequest.url === baseURL + "token/refresh/"
) {
window.location.href = "/login/";
return Promise.reject(error);
}
if (
error.response.data.code === "token_not_valid" &&
error.response.status === 401 &&
error.response.statusText === "Unauthorized"
) {
const refreshToken = localStorage.getItem("refreshtoken");
if (refreshToken) {
const tokenParts = JSON.parse(atob(refreshToken.split(".")[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
console.log(tokenParts.exp);
if (tokenParts.exp > now) {
return axioAnonymousInstance
.post("/api/token/refresh/", { refresh: refreshToken })
.then((response) => {
localStorage.setItem("accesstoken", response.data.access);
localStorage.setItem("refreshtoken", response.data.refresh);
axiosInstance.defaults.headers["Authorization"] =
"JWT " + response.data.access;
originalRequest.headers["Authorization"] =
"JWT " + response.data.access;
return axiosInstance(originalRequest);
})
.catch((err) => {
// redirect ro /login here if wanted
console.log("axios Safe Instance error");
console.log(err);
// window.location.href = "/login/";
});
} else {
console.log("Refresh token is expired", tokenParts.exp, now);
window.location.href = "/login/";
}
} else {
console.log("Refresh token not available.");
window.location.href = "/login/";
}
}
// specific error handling done elsewhere
return Promise.reject(error);
}
);
export { axiosInstance, axioAnonymousInstance };
( I know I shouldn't use localStorage but whatever )
and I would typically just call this function to make the simple request to the view written above :
const IsConnected = () => {
axiosInstance
.get("/api/is_connected/")
.then((response) => {
if (response.status === 200) {
console.log(response.data);
console.log("Is connected : CONNECTED ");
} else {
console.log("IS connected : not connected");
}
})
.catch((error) => {
console.log("Is connected : NOT CONNECTED");
console.log(error);
});
};
Without the specifics of the exact query hit your db, it's hard to tell what is happening (the db query must have originated from a middleware because there's nothing in your code that does it, and I suspect it's django's CsrfViewMiddleware). However, as for your question of JWT being stateless, I suggest you to take a look at the official introduction.
Basically, what happens with a JWT is that your server performs a signature verification on the token using your server's secret key (please beware of some problems). If the verification passes, then the data stored inside the JWT is trusted and read as is, which is why no database query is necessary. Of course, this does mean that your user will know exactly what is stored inside their token because the data is a simple base64 encoded JSON object.
Apollo link offers an error handler onError
Issue:
Currently, we wish to refresh oauth tokens when they expires during an apollo call and we are unable to execute an async fetch request inside the onError properly.
Code:
initApolloClient.js
import { ApolloClient } from 'apollo-client';
import { onError } from 'apollo-link-error';
import { ApolloLink, fromPromise } from 'apollo-link';
//Define Http link
const httpLink = new createHttpLink({
uri: '/my-graphql-endpoint',
credentials: 'include'
});
//Add on error handler for apollo link
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
//User access token has expired
if(graphQLErrors[0].message==="Unauthorized") {
//We assume we have both tokens needed to run the async request
if(refreshToken && clientToken) {
//let's refresh token through async request
return fromPromise(
authAPI.requestRefreshToken(refreshToken,clientToken)
.then((refreshResponse) => {
let headers = {
//readd old headers
...operation.getContext().headers,
//switch out old access token for new one
authorization: `Bearer ${refreshResponse.access_token}`,
};
operation.setContext({
headers
});
//Retry last failed request
return forward(operation);
})
.catch(function (error) {
//No refresh or client token available, we force user to login
return error;
})
)
}
}
}
}
}
}),
What happens is:
Initial graphQL query runs and fails due to unauthorization
The onError function of ApolloLink is executed.
The promise to refresh the token is executed.
The onError function of ApolloLink is executed again??
The promise to refresh the token is completed.
The initial graphQL query result is returned and its data is undefined
Between step 5 and 6, apollo doesn't re-run the initial failed graphQL query and hence the result is undefined.
Errors from console:
Uncaught (in promise) Error: Network error: Error writing result to store for query:
query UserProfile($id: ID!) {
UserProfile(id: $id) {
id
email
first_name
last_name
}
__typename
}
}
The solution should allow us to:
Run an async request when an operation fails
Wait for the result of the request
Retry failed operation with data from the request's result
Operation should succeed to return its intended result
I'm refreshing the token this way (updated OP's):
import { ApolloClient } from 'apollo-client';
import { onError } from 'apollo-link-error';
import { ApolloLink, Observable } from 'apollo-link'; // add Observable
// Define Http link
const httpLink = new createHttpLink({
uri: '/my-graphql-endpoint',
credentials: 'include'
});
// Add on error handler for apollo link
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError, operation, forward }) => {
// User access token has expired
if (graphQLErrors && graphQLErrors[0].message === 'Unauthorized') {
// We assume we have both tokens needed to run the async request
if (refreshToken && clientToken) {
// Let's refresh token through async request
return new Observable(observer => {
authAPI.requestRefreshToken(refreshToken, clientToken)
.then(refreshResponse => {
operation.setContext(({ headers = {} }) => ({
headers: {
// Re-add old headers
...headers,
// Switch out old access token for new one
authorization: `Bearer ${refreshResponse.access_token}` || null,
}
}));
})
.then(() => {
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
};
// Retry last failed request
forward(operation).subscribe(subscriber);
})
.catch(error => {
// No refresh or client token available, we force user to login
observer.error(error);
});
});
}
}
})
])
});
Accepted answer is quite good but it wouldn't work with 2 or more concurrent requests. I've crafted the one below after testing different cases with my token renew workflow that fits my needs.
It's necessary to set errorLink before authLink in link pipeline.
client.ts
import { ApolloClient, from, HttpLink } from '#apollo/client'
import errorLink from './errorLink'
import authLink from './authLink'
import cache from './cache'
const httpLink = new HttpLink({
uri: process.env.REACT_APP_API_URL,
})
const apiClient = new ApolloClient({
link: from([errorLink, authLink, httpLink]),
cache,
credentials: 'include',
})
export default apiClient
Cache shared between 2 apollo client instances for setting user query when my renewal token is expired
cache.ts
import { InMemoryCache } from '#apollo/client'
const cache = new InMemoryCache()
export default cache
authLink.ts
import { ApolloLink } from '#apollo/client'
type Headers = {
authorization?: string
}
const authLink = new ApolloLink((operation, forward) => {
const accessToken = localStorage.getItem('accessToken')
operation.setContext(({ headers }: { headers: Headers }) => ({
headers: {
...headers,
authorization: accessToken,
},
}))
return forward(operation)
})
export default authLink
errorLink.ts
import { ApolloClient, createHttpLink, fromPromise } from '#apollo/client'
import { onError } from '#apollo/client/link/error'
import { GET_CURRENT_USER } from 'queries'
import { RENEW_TOKEN } from 'mutations'
import cache from './cache'
let isRefreshing = false
let pendingRequests: Function[] = []
const setIsRefreshing = (value: boolean) => {
isRefreshing = value
}
const addPendingRequest = (pendingRequest: Function) => {
pendingRequests.push(pendingRequest)
}
const renewTokenApiClient = new ApolloClient({
link: createHttpLink({ uri: process.env.REACT_APP_API_URL }),
cache,
credentials: 'include',
})
const resolvePendingRequests = () => {
pendingRequests.map((callback) => callback())
pendingRequests = []
}
const getNewToken = async () => {
const oldRenewalToken = localStorage.getItem('renewalToken')
const {
data: {
renewToken: {
session: { renewalToken, accessToken },
},
},
} = await renewTokenApiClient.mutate({
mutation: RENEW_TOKEN,
variables: { input: { renewalToken: oldRenewalToken } },
})!
localStorage.setItem('renewalToken', renewalToken)
localStorage.setItem('accessToken', accessToken)
}
const errorLink = onError(({ graphQLErrors, operation, forward }) => {
if (graphQLErrors) {
for (const err of graphQLErrors) {
switch (err?.message) {
case 'expired':
if (!isRefreshing) {
setIsRefreshing(true)
return fromPromise(
getNewToken().catch(() => {
resolvePendingRequests()
setIsRefreshing(false)
localStorage.clear()
// Cache shared with main client instance
renewTokenApiClient!.writeQuery({
query: GET_CURRENT_USER,
data: { currentUser: null },
})
return forward(operation)
}),
).flatMap(() => {
resolvePendingRequests()
setIsRefreshing(false)
return forward(operation)
})
} else {
return fromPromise(
new Promise((resolve) => {
addPendingRequest(() => resolve())
}),
).flatMap(() => {
return forward(operation)
})
}
}
}
}
})
export default errorLink
We just had the same issues and after a very complicated solution with lots of Observeables we got a simple solution using promises which will be wrapped as an Observable in the end.
let tokenRefreshPromise: Promise = Promise.resolve()
let isRefreshing: boolean
function createErrorLink (store): ApolloLink {
return onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
// this is a helper method where we are checking the error message
if (isExpiredLogin(graphQLErrors) && !isRefreshing) {
isRefreshing = true
tokenRefreshPromise = store.dispatch('authentication/refreshToken')
tokenRefreshPromise.then(() => isRefreshing = false)
}
return fromPromise(tokenRefreshPromise).flatMap(() => forward(operation))
}
if (networkError) {
handleNetworkError(displayErrorMessage)
}
})
}
All pending requests are waiting for the tokenRefreshPromise and will then be forwarded.