We are currently implemented ADAL.js using Angular, but unfortunately the token does not renew automatically, the code is the following to handle the logic to renew the token
We added some logic to check if the token is expired and renew it automatically, but this is not working for me. The application kick you out to white page, and it is required to refresh the browser in order sign in again.
#Injectable({
providedIn: 'root',
})
export class AuthenticationService {
checkAuthInterval$: Observable<number>;
constructor(
private jwtHelperService: JwtHelperService,
private adal: MsAdalAngular6Service
) {
this.checkAuthInterval$ = interval(6000).pipe(
tap(() => {
this.updateSession();
})
);
this.checkAuthInterval$.subscribe();
}
public isAuthorized(allowedRoles: string[]): boolean {
if (allowedRoles == null || allowedRoles.length === 0) {
return true;
}
const token = localStorage.getItem(
authenticationSettings.authenticationToken
);
const status = this.jwtHelperService.isTokenExpired(token);
const decodeToken = this.jwtHelperService.decodeToken(token);
if (!decodeToken) {
console.log('Invalid token');
return false;
}
decodeToken.roles = decodeToken.roles || [];
const allow = allowedRoles.some(r => decodeToken.roles.includes(r));
return allow;
}
get profileInfo(): User {
return this.adal.userInfo;
}
get expirationDate() {
const expiration = this.profileInfo.profile.exp * 1000;
return moment(expiration);
}
get issueDate() {
const expiration = this.profileInfo.profile.iat * 1000;
return moment(expiration);
}
get hasExpired(): boolean {
const today = moment();
return this.expirationDate.isSameOrBefore(today);
}
get shouldRenew(): boolean {
const today = moment().subtract(5, 'minutes');
return this.expirationDate.isSameOrBefore(today);
}
get isAuthenticated() {
const authenticated = this.adal.isAuthenticated;
return authenticated;
}
updateSession() {
if (this.hasExpired) {
this.adal.login();
} else if (this.shouldRenew) {
this.adal.RenewToken(environment.ApiBaseUrl);
}
}
}
Generally speaking, we don't recommend trying to renew the token yourself, as you should let the library handle that as needed. Our recommendation for acquiring tokens is as follows:
Check if the user has signed in, and if not, send them to the login screen: https://github.com/AzureAD/azure-activedirectory-library-for-js#2-login-the-user
When your app is in the process of making a network request to a resource (e.g. Microsoft Graph) that requires an access token, call acquireToken with the scopes needed for that request.
If there is already a valid token in the cache, that token should be returned to you.
If there is not a valid token in the cache (e.g. an expired token) and your user still has an active session with AAD, the library will make a network request to acquire a new token, and return that new token to you.
If there is not a valid token in the cache, and your user does not have an active session with AAD or there is some other scenario that requires interaction (e.g. consenting to new scopes), the library will return an error and you will need to invoke one of the interactive methods (acquireTokenPopup or acquireTokenRedirect) to resolve those issues, and then the new token will be returned to your application: https://github.com/AzureAD/azure-activedirectory-library-for-js#3-get-an-access-token
Automatically trying to renew the token yourself may cause other issues, which is why we don't recommend it. Instead, call acquireToken lazily right before you need to the token, and then have error handling in place (as described above) to invoke interactive methods if needed.
Have you tried the process described above, and if so, what were the specific issues/errors that you encountered? Are there errors in the browser console or the network tab?
Related
I'm having troubled sending an authenticated request to my API immediately after signing in to my Nextjs app using NextAuth. The request that is sent after signing in returns data for and unauthenticated user.
I believe the issue is that React Query is using a previous version of the query function with an undefined jwt (which means its unauthenticated). It makes sense because the query key is not changing so React Query does not think it's a new query, but, I was under the impression that signing in would cause loading to be set to true temporarily then back to false, which would cause React Query to send a fresh request.
I've tried invalidating all the queries in the app using queryClient, but that did not work. I've also used React Query Devtools to invalidate this specific query after signing in but it still returns the unauthenticated request. Only after refreshing the page does it actually send the authenticated request.
// useGetHome.js
const useGetHome = () => {
const [session, loading] = useSession();
console.log(`session?.jwt: ${session?.jwt}`);
return useQuery(
'home',
() => fetcher(`/home`, session?.jwt),
{
enabled: !loading,
},
);
}
// fetcher
const fetcher = (url, token) => {
console.log(`token: ${token}`);
let opts = {};
if (token) {
opts = {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}${url}`, opts);
if (!res.ok) {
const error = await res.json();
throw new Error(error.message);
}
return res.json();
}
// Home.js
const Home = () => {
const { data: home_data, isLoading, error } = useGetHome();
...
return(
...
)
}
Attached is the console immediately after signing in. You can see the the session object contains the jwt after signing in, but in the fetcher function it is undefined.
console after signing in
Any help here is appreciated. Is there a better way to handle authenticated requests using React Query and NextAuth? Thank you!
I have tried a similar situation here and struggled the same thing but the enabled property worked fine for me and it is good to go right now.
https://github.com/maxtsh/music
Just check my repo to see how it works, that might help.
I like to understand the JWT handling of token.
I have created a login page to check if user exist in DB? If yes, I used jwt sign a token and return jwt token.
jwt.sign({userdata}, secretKey, (err, token) => {
res.json({
token
After I get the token I understand I have store it in local storage.
localStorage.setItem("token", token);
After this I am lost! How can I redirect the login to a protected URL once the token is stored?
Then my next question is how can I make use of the local stored token in the protected route?
For example login.html will invoke a login function call and return the token then I want to go to /admin/admin.html. In /admin/admin.html, i have protected routes that need to use the token. How can I use it ? How can I know the user is the same user using the protected route since? I know the localstored token has the user information. Does that mean every protected route I have to post a user information and compare to local token?
Some examples of the code will be useful. Thanks
You can do something like that
login() {
try{
const tk = response.token; // from api response
if (tk) {
const expiresInDuration = response.expiresIn;
setAuthTimer(expiresInDuration); // setTimer to not send rest call everytime if user is visiting many times
const now = new Date();
const expirationDate = new Date(
now.getTime() + expiresInDuration * 1000
);
this.saveAuthData(this.token, expirationDate, role);
navigate(['/home']); // function which should redirect to your desired url
}
}, (err) => {
console.log(err)
});
}
// function to auto logout after specified time
setAuthTimer(duration: number) {
this.tokenTimer = setTimeout(() => {
this.logout();
}, duration * 1000);
}
saveAuthData(token, expirationDate, role) {
localStorage.setItem('token', token);
localStorage.setItem('expiration', expirationDate.toISOString());
}
// after delete and log out
clearAuthData() {
localStorage.removeItem('token');
localStorage.removeItem('expiration');
}
// function to login user if its data is already present in the localStorage
autoAuthUser() {
authInformation = getAuthData();
if (authInformation) {
const now = new Date();
const expiresIn = authInformation.expirationDate.getTime() - now.getTime();
if (expiresIn > 0) {
this.token = authInformation.token;
this.isAuthenticated = true;
this.setAuthTimer(expiresIn / 1000);
}
}
}
For your question regarding same user is accessing the protected route as local storage is storing token specific to user that should take care of the task
You have to use a library that verifies your stored JWT token. You can use https://www.npmjs.com/package/jsonwebtoken . This library includes a method that verifies your JWT jwt.verify(token, secretOrPublicKey, [options, callback]). To be able to verify a token, you must provide the secret key that is used to sign your tokens. If the token is verified successfully, you can redirect the user to its designated page. As long as the token is stored and not expired, the user is remembered in the browser.
This is an approach for JS apps, however, if you're using PHP/Laravel, the token is stored in a HTTP cookie and I recommend using jwt-auth library, it will handle the JWT processes for you.
I have a problem with the function(I suspect) in which I add custom claims. Here is the code:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.addAdminRole = functions.https.onCall((data)=> {
//get user and add custom claim (admin)
return admin.auth().getUserByEmail(data.email).then(user => {
return admin.auth().setCustomUserClaims(user.uid, {
admin: true
});
}).then( () => {
return {
message: `Success! ${data.email} has been made an admin`,
}
})
})
I call the function using the following code(I use Redux and React):
let addAdminRole = window.firebaseFunctions.httpsCallable('addAdminRole');
addAdminRole({email:employee.email}).then(res => {
console.log(res)
})
I get the expected message({message: Success! ${data.email} has been made an admin}), but the claim isn't added.
I make a separate Firebase Auth REST API via axios for authentication, but the claim 'admin' isn't there.
I have a Spark(free) billing plan and when checking the logs from firebase functions I see 'Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions' when addAdminRole is executed.
From what I read this is a message that you always get on the free plan and there shouldn't be a problem when accessing internal(google) info.
Here is the code for the axios Request:
axios({
method:'post',
url:urlAuth,
data:{
email:employee.email,
password:employee.password,
returnSecureToken: true
}
}).then(res => {
delete employee.password;
console.log(res)
const expirationDate = new Date().getTime() + res.data.expiresIn * 1000;
localStorage.setItem('token',res.data.idToken);
localStorage.setItem('expirationDate',expirationDate);
localStorage.setItem('userId', res.data.localId);
localStorage.setItem('admin', res.data.admin)
dispatch(checkAuthTimeout(res.data.expiresIn));
if(logIn){
dispatch(endAuth(res.data.idToken,res.data.localId,res.data.admin));
}else{
employee.userId = res.data.localId;
dispatch(addEmplRegister(employee,res.data.idToken,admin));
}
}).catch(err => {
dispatch(errorAuth(err.message))
})
FOUND OUT THE ISSUE, the information about claims isn't transmitted when using REST API authentication
Setting a custom claim doesn't take effect immediately for users with existing JWT tokens. Those users will have to either:
Sign out and back in again,
Force refresh their token, or
Wait up to one hour for that token to automatically refresh by the Fireabse Auth SDK.
On then will their new token show the custom claim.
I'm working on an Ionic app and trying to cash in the refresh token when a user gets a 401 response on an HTTP request. I found a few examples floating around online and was able to get this one (https://www.intertech.com/Blog/angular-4-tutorial-handling-refresh-token-with-new-httpinterceptor/) working with the exception of multiple requests coming in at once.
The problem I'm having is the first call in the series of calls invokes the refresh token and retries successfully, while the other ones never get retried. If I take the .filter and .take off the subject return for requests where a refresh is already in progress, the calls do get retried but without the new token. I'm pretty new when it comes to observables and subjects so I'm not really sure what the problem could be.
requests
this.myService.getData().subscribe(response => {this.data = response.data;});
this.myService.getMoreData().subscribe(response => {this.moreData = response.data;});
this.myService.getEvenMoreData().subscribe(response => {this.evenMoreData = response.data;});
interceptor
#Injectable()
export class HttpInterceptor implements HttpInterceptor {
isRefreshingToken: boolean = false;
tokenSubject = new BehaviorSubject<string>(null);
tokenService: tokenService;
constructor(private authService: AuthService, private injector: Injector) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
return this.authService.getUser().flatMap(user => {
request = this.addToken(request, next, user.accessToken);
return next
.handle(request)
.catch(error => {
if (error instanceof HttpErrorResponse) {
switch ((<HttpErrorResponse>error).status) {
case 401:
return this.handle401(request, next, user);
}
} else {
return Observable.throw(error);
};
})
});
}
addToken(request: HttpRequest<any>, next: HttpHandler, accessToken: string): HttpRequest<any> {
return request.clone({ setHeaders: { Authorization: 'Bearer ' + accessToken }})
}
handle401(request: HttpRequest<any>, next: HttpHandler, user: any) {
if (!this.isRefreshingToken) {
this.isRefreshingToken = true;
this.tokenSubject.next(null);
this.tokenService = this.injector.get(tokenService);
return this.tokenService.refresh(user.refreshToken)
.switchMap(refreshResponse => {
if (refreshResponse) {
this.authService.setUser(refreshResponse.id_token, refreshResponse.access_token, refreshResponse.refresh_token);
this.tokenSubject.next(refreshResponse.accessToken);
return next.handle(this.addToken(request, next, refreshResponse.access_token));
}
else {
//no token came back. probably should just log user out.
}
})
.finally(() => {
this.isRefreshingToken = false;
});
}
else {
return this.tokenSubject
.filter(token => token != null)
.take(1)
.switchMap(token => {
return next.handle(this.addToken(request, next, token));
});
}
}
}
It looks to me like you didn't have the right token:
You had:
this.tokenSubject.next(refreshResponse.accessToken);
Should be:
this.tokenSubject.next(refreshResponse.access_token);
I actually ended up solving this by moving the subject to my auth service and doing a next in the setUser method. Then in the else statement in my 401 method, I returned the subject from a new method on my auth service and that fixed it. I still needed the take(1) but was able to get rid of the filter since I ended up not using a BehaviorSubject.
I faced a similar issue in the past. For some unknown reason (at least to me), when I intercept the 401, I make the refresh and retry, but retry operation goes cancelled.
Nevertheless, I realised that I can read the JWT expiration on client-side, so I tricked the system by saving the token expiration time. I then made routing events (say onViewWillEnter) check the expiration and, if token expired, refresh it.
This mechanism is totally transparent to the user, ensures that auth token nor refresh token expire if the user stays too long without performing HTTP requests and, most importantly, reduces latencies as you never get a 401 response (which, in your scenario, translates to three requests).
One simple way to achieve this is by means of a guard:
canActivate(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot) {
if (this.refreshTokenService.isExpired) {
this.tokenEvent_.next();
return false;
} else {
this.refreshTokenService.refresh();
}
where refreshTokenService is a utility service that has the tokens and a method for performing refresh via HTTP. tokenEvent is a rxjs/Subject: it is subscribed in guard constructor and each time a new event comes, it redirects to login page.
Adding this guard on every route ensures that the token is always non-expired.
How to create middleware which will catch all errors, for example I have request which required token, token can expired or damaged, so I need catch this errors on every request and be able to call queries and mutations.
For example:
On expired token, I must refetch token and repeat request.
On token damaged, I must logout user and refetch all queries.
And type of error witch I need to handle can be many.
In(react-apollo docs)
networkInterface.useAfter([{
applyAfterware({ response }, next) {
if (response.status === 401) {
logout();
}
next();
}
}]);
I can't access to graphql error, and call queries or mutations.
You can check to see if you have a token before every request is sent. If you do not have a token, you should handle that somewhere else in your application or potentially fetch another straight from the middleware function. You could make higher order component that wraps all of your components that must have a token. If for some reason there is no token, you can fetch another one and store it to localStorage if you are using the browser or asyncstorage if you are using React Native. Once you've assigned it to localStorage or asyncStorage, this middleware code snippet below will check for the token before every request you send, this includes all queries and mutations. If you find that your user doesn't have a token, you could also redirect them in your component them to a page where they must login again and then from there set the token to localstorage or asynstorage. Once again the apollo client's middleware will have access to it that way.
import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { checkForSessionToken } from '../../utils/authentication';
const networkInterface = createNetworkInterface({
uri: 'https://localhost:4000'
});
networkInterface.use([{
applyMiddleware(req, next) {
// Create the header object if needed.
if (!req.options.headers) {
req.options.headers = {};
}
// get the authentication token from Async storage
// and assign it to the request object
checkForSessionToken()
.then(SESSION_TOKEN => {
if (SESSION_TOKEN === null || SESSION_TOKEN === undefined {
fetchNewToken()
.then(SESSION_TOKEN => {
localStorage.setItem('token', SESSION_TOKEN);
req.options.headers.Authorization = `Bearer
${SESSION_TOKEN}`;
}
} else {
req.options.headers.Authorization = `Bearer ${SESSION_TOKEN}`;
}
next();
})
.catch(error => {
fetchNewToken()
.then(SESSION_TOKEN => {
localStorage.setItem('token', token);
req.options.headers.Authorization = `Bearer
${SESSION_TOKEN}`;
}
next();
})
}
}]);
const client = new ApolloClient({
networkInterface,
dataIdFromObject: o => o.id
});
export default client;