Silent refresh not working with OIDC-client in Angular 5 - oidc-client-js

I have an issue with the silent refresh with oidc-client.
The signin works fine and I'm able to acquire a token.
However, the silent refresh doesn't fire, nothing happens. When I subscribe to methods that check token expiry (methods in subscribeevents in authservice.ts below), these methods never fire - and the method isLoggedIn() always return true even if the token has expired.
Here is my code :
import { Component, OnInit } from '#angular/core';
import { UserManager } from 'oidc-client';
import { getClientSettings } from '../openIdConnectConfig';
import { AuthService } from '../services/auth.service';
#Component({
selector: 'app-silentrefresh',
templateUrl: './silentrefresh.component.html',
styleUrls: ['./silentrefresh.component.css']
})
export class SilentRefreshComponent implements OnInit {
constructor(private _authService:AuthService) {
}
ngOnInit() {
this._authService.refreshCallBack();
}
}
Then my authservice :
import { UserManagerSettings, UserManager, User } from 'oidc-client';
import { Injectable } from '#angular/core';
import { getClientSettings } from '../openIdConnectConfig';
#Injectable()
export class AuthService {
private _manager = new UserManager(getClientSettings());
private _user: User = null;
constructor() {
this._manager.getUser().then(user => {
this._user = user;
});
this._manager.events.addUserLoaded(user => {
this._user = user;
});
this.subscribeevents();
}
public isLoggedIn(): boolean {
return this._user != null && !this._user.expired;
}
public getClaims(): any {
return this._user.profile;
}
public subscribeevents(): void {
this._manager.events.addSilentRenewError(() => {
console.log("error SilentRenew");
});
this._manager.events.addAccessTokenExpiring(() => {
console.log("access token expiring");
});
this._manager.events.addAccessTokenExpired(() => {
console.log("access token expired");
});
}
public refreshCallBack(): void {
console.log("start refresh callback");
this._manager.signinSilentCallback()
.then(data => { console.log("suucess callback") })
.catch(err => {
console.log("err callback");
});
console.log("end refresh callback");
}
getUser(): any {
return this._user;
}
getName(): any {
return this._user.profile.name;
}
getAuthorizationHeaderValue(): string {
return `${this._user.token_type} ${this._user.access_token}`;
}
startAuthentication(): Promise<void> {
return this._manager.signinRedirect();
}
completeAuthentication(): Promise<void> {
return this._manager.signinRedirectCallback().then(user => {
this._user = user;
});
}
}
And my config:
import { UserManagerSettings } from "oidc-client";
export function getClientSettings(): UserManagerSettings {
return {
authority: 'https://login.microsoftonline.com/136544d9-038e-4646-afff-10accb370679',
client_id: '257b6c36-1168-4aac-be93-6f2cd81cec43',
redirect_uri: 'http://localhost:4200/auth-callback',
//redirect_uri: 'https://demoazureadconnectangular5.azurewebsites.net/auth-callback',
post_logout_redirect_uri: 'http://localhost:4200/',
//post_logout_redirect_uri: 'https://demoazureadconnectangular5.azurewebsites.net/',
response_type: "id_token",
scope: "openid profile",
filterProtocolClaims: true,
loadUserInfo: true,
automaticSilentRenew: true,
silent_redirect_uri: 'http://localhost:4200/assets/silentrefresh',
metadata: {
issuer: "https://sts.windows.net/136544d9-038e-4646-afff-10accb370679/",
authorization_endpoint: "https://login.microsoftonline.com/136544d9-038e-4646-afff-10accb370679/oauth2/authorize",
token_endpoint: "https://login.microsoftonline.com/136544d9-038e-4646-afff-10accb370679/oauth2/token",
//jwks_uri: "https://login.microsoftonline.com/common/discovery/keys",
jwks_uri: "http://localhost:4200/assets/keys.json",
//jwks_uri: "https://demoazureadconnectangular5.azurewebsites.net/assets/keys.json",
//jwks_uri: "http://localhost:50586/api/keys",
signingKeys: [{ "ApiAccessKey": "NgixniZ0S1JHxo7GPEZYa38OBTxSA98AqJKDX5XqsJ8=" }]
}
};
}
I also tried to use a static page like this:
<head>
<title></title>
</head>
<body>
<script src="oidc-client.min.js"></script>
<script>
var usermanager = UserManager().signinSilentCallback()
.catch((err) => {
console.log(err);
});
</script>
</body>
It's never fired neither
In order to test, I've changed the ID token expiry to 10 min.
I use Azure AD Connect (Open Id Connect in Azure) and Microsoft says it's not fully compatible with Open ID Connect standard... So I don't know if it's on my side or Azure side.
Somebody can help me to solve this?

The problem is that you are not asking access_token from azure AD, only id_token. You must set response_type to id_token token to get both tokens. This change will need also few more parameters. For example resource for your backend.
I have answered similar question here. I'm using also Angular 5 and oidc client. https://stackoverflow.com/a/50922730/8081009
And I answer you here also before https://github.com/IdentityModel/oidc-client-js/issues/504#issuecomment-400056662
Here is what you need to set to get silent renew working.
includeIdTokenInSilentRenew: true
extraQueryParams: {
resource: '10282f28-36ed-4257-a853-1bf404996b18'
}
response_type: 'id_token token',
scope: 'openid'
loadUserInfo: false,
automaticSilentRenew: true,
silent_redirect_uri: `${window.location.origin}/silent-refresh.html`,
metadataUrl: 'https://login.microsoftonline.com/YOUR_TENANT_NAME.onmicrosoft.com/.well-known/openid-configuration',
signingKeys: [
add here keys from link below
]
https://login.microsoftonline.com/common/discovery/keys
I'm also using different static page for callback endpoint with silent renew because this way user won't notice a thing. This page is minimum possible so oidc won't load whole angular application to hidden iframe what it is using for silent renew. So this is recommended to be more efficient.
<head>
<title></title>
</head>
<body>
<script src="assets/oidc-client.min.js"></script>
<script>
new Oidc.UserManager().signinSilentCallback()
.catch((err) => {
console.log(err);
});
</script>
</body>

Check if you have correct redirect URI in the database.
Check you have added the following in your angular.json file:
...
"assets": [
"src/assets",
"silent-refresh.html",
"oidc-client.min.js"
.....
],
...
Check silent-refresh.html:
<script src="oidc-client.min.js"></script><script>
var mgr = new Oidc.UserManager();
mgr.signinSilentCallback().catch(error => {
console.error(error);
});
</script>
Check you do not create more than one instance of UserManager
You can do either way - automaticSilentRenew: false, or automaticSilentRenew: true, I will recommend using automaticSilentRenew: false and trigger an event on expiring.
https://github.com/IdentityModel/oidc-client-js/wiki
public renewToken() {
return this.manager.signinSilent().then(u => {
this.user = u;
}).catch(er => {
console.log(er);
});
}
this.manager.events.addAccessTokenExpiring(x => {
console.log('Acess token expiring event');
this.renewToken().then(u => {
console.log('Acess token expiring event renew success');
});
});
If the above things do not work then check the identity server code.
Identity server code
Startup
services.AddIdentityServer(options =>
{
options.Authentication.CookieLifetime = TimeSpan.FromDays(30);
options.Authentication.CookieSlidingExpiration = true;
});
services.AddAuthentication(x => x.DefaultAuthenticateScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme);
Logout
await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.DefaultCookieAuthenticationScheme);
Thanks to https://github.com/IdentityModel/oidc-client-js/issues/911#issuecomment-617724445

Simplest reason can be, not adding silent renew url as a redirect url in identity server configuration.
In your identity server database, redirect urls for your clients should be like this
redirectUrls: [http://localhost:4200/assets/silentrefresh, http://localhost:4200/auth-callback]

I have used some different approach to initate the silentRenw, instead of using
automaticSilentRenew: true,
I decided to explicitly call the signInSilent();.
Reason for doing the i was facing some issues as automaticSilentRenew: true, was not working as expected.
I initialized the event and method in my UserAuth class constructor that implements my interface
constructor(private oidcSettings: CoreApi.Interfaces.Authentication.OpenIDConnectionSettings)
{
this.userManager.events.addAccessTokenExpiring(() =>
{
this.userManager.signinSilent({scope: oidcSettings.scope, response_type: oidcSettings.response_type})
.then((user: Oidc.User) =>
{
this.handleUser(user);
})
.catch((error: Error) =>
{
//Work around to handle to Iframe window timeout errors on browsers
this.userManager.getUser()
.then((user: Oidc.User) =>
{
this.handleUser(user);
});
});
});
}
Where as handleUser is just check for logged in user.
So if you initialize the signInSilent process in your constructor and then call signInSilent complete i.e. callback it may work.

Not sure what oidc-client.js version you are using, this should never have worked.
```
new Oidc.UserManager().signinSilentCallback()
.catch((err) => {
console.log(err);
});
``
Usermanager is in **Oidc** object.

Related

"TypeError: OAuth2Strategy requires a verify callback." but i send it (nestjs)

I call the constructor of OAuth2Strategy in app.controller.ts, i send it the 2 params it needs: options with clientID, callbackURL etc, and a verify callback function. But i have this error, looks like i don't send a verify callback function to the constructor, but i did. The error happens in node_modules/passport-oauth2/lib/strategy.js.
app.controller.ts:
import { Body, Controller, Get, Post, Query, Res, Req, Next, UnauthorizedException, UseGuards } from '#nestjs/common';
import { PartieService, JoueurService, CanalService } from './app.service';
import { Joueur, Partie, Canal } from './app.entity';
import { AuthGuard } from '#nestjs/passport';
import passport = require("passport");
import OAuth2Strategy = require("passport-oauth2");
//var OAuth2Strategy = require('passport-oauth2').OAuth2Strategy;
//import OAuth2Strategy from 'passport-oauth2';
#Controller()
export class AppController {
constructor(private readonly partieService: PartieService, private readonly joueurService: JoueurService,
private readonly canalService: CanalService) {}
#Get('/oauth2')
async oauth2(#Req() req, #Res() res, #Next() next) {
passport.use('oauth2', new OAuth2Strategy(
{
clientID: 'my_clientID',
clientSecret: 'my_clientSecret',
authorizationURL: 'https://api.abc.com/oauth/authorize',
tokenURL: 'https://api.abc.fr/oauth/token',
callbackURL: 'http://localhost:3000/Connection'
},
async (accessToken: string, refreshToken: string, profile: any, done: Function) => {
//console.log(profile);
try {
if (!accessToken || !refreshToken || !profile) {
return done(new UnauthorizedException(), false);
}
const user: Joueur = await this.joueurService.findOrCreate(profile);
return done(null, user);
} catch (err) {
return done(new UnauthorizedException(), false);
}
} ));
passport.authenticate('oauth2');
return res.sendFile("/home/user42/Documents/Projets/ft_transcendence/services/pong/pong/public/connection.html");
}
#Get('/Connection')
//#UseGuards(AuthGuard('oauth2'))
async Callback(#Req() req, #Res() res, #Next() next) {
passport.authenticate('oauth2', (err, user, info) => {
if (err || !user) {
req.session.destroy();
return res.status(401).json({
message: 'Unauthorized',
});
}
req.user = user;
return next();
})(req, res, next);
const utilisateur: Joueur = req.user;
console.log(utilisateur);
}
[...]
}
node_modules/passport-oauth2/lib/strategy.js:
function OAuth2Strategy(options, verify) {
if (typeof options == 'function') {
verify = options;
options = undefined;
}
options = options || {};
if (!verify) { throw new TypeError('OAuth2Strategy requires a verify callback'); }
[...]
}
Error:
[Nest] 6550 - 05/02/2023, 18:34:54 ERROR [ExceptionHandler] OAuth2Strategy requires a verify callback
TypeError: OAuth2Strategy requires a verify callback
at new OAuth2Strategy (/home/user42/Documents/Projets/ft_transcendence/services/pong/pong/node_modules/passport-oauth2/lib/strategy.js:84:24)
at Injector.instantiateClass (/home/user42/Documents/Projets/ft_transcendence/services/pong/pong/node_modules/#nestjs/core/injector/injector.js:340:19)
at callback (/home/user42/Documents/Projets/ft_transcendence/services/pong/pong/node_modules/#nestjs/core/injector/injector.js:53:45)
at Injector.resolveConstructorParams (/home/user42/Documents/Projets/ft_transcendence/services/pong/pong/node_modules/#nestjs/core/injector/injector.js:132:24)
at Injector.loadInstance (/home/user42/Documents/Projets/ft_transcendence/services/pong/pong/node_modules/#nestjs/core/injector/injector.js:57:13)
at Injector.loadProvider (/home/user42/Documents/Projets/ft_transcendence/services/pong/pong/node_modules/#nestjs/core/injector/injector.js:84:9)
at async Promise.all (index 3)
at InstanceLoader.createInstancesOfProviders (/home/user42/Documents/Projets/ft_transcendence/services/pong/pong/node_modules/#nestjs/core/injector/instance-loader.js:47:9)
at /home/user42/Documents/Projets/ft_transcendence/services/pong/pong/node_modules/#nestjs/core/injector/instance-loader.js:32:13
at async Promise.all (index 1)
`
I tried to change the import OAuth2Strategy many times (like the commented lines). I tried a lot of things but i cannot remember all. I found no answer in internet, and no more with the not-so-well-chatgpt.
I am new in nestjs and this error is weird for me, how can it ask me a parameter that i send ? Can someone help me to resolve this pls ? :)
Sorry if the answer is obvious :/ , it's my first API with nestjs
The error was coming from another file, my bad, it's fixed.

Axios interceptor changes the POST request's content type on 401 retry

I cannot figure out why Axios is changing my request's content-type on retry.
I am creating an axios instance as follows (notice global default header):
import axios, { type AxiosInstance } from "axios";
const api: AxiosInstance = axios.create({
baseURL: "https://localhost:44316/",
});
export default api;
I import this instance in various components within my vue3 app. When my token has expired and I detect a 401, I use the interceptor to refresh my token and retry the call as follows (using a wait pattern to queue multiple requests and prevent requesting multiple refresh tokens):
axios.interceptors.request.use(
(config) => {
const authStore = useAuthStore();
if (!authStore.loggedIn) {
authStore.setUserFromStorage();
if (!authStore.loggedIn) {
return config;
}
}
if (config?.headers && authStore.user.accessToken) {
config.headers = {
Authorization: `Bearer ${authStore.user.accessToken}`,
};
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
(res) => {
return res;
},
async (err) => {
if (err.response.status === 401 && !err.config._retry) {
console.log("new token required");
err.config._retry = true;
const authStore = useAuthStore();
if (!authStore.isRefreshing) {
authStore.isRefreshing = true;
return new Promise((resolve, reject) => {
console.log("refreshing token");
axios
.post("auth/refreshToken", {
token: authStore.user?.refreshToken,
})
.then((res) => {
authStore.setUserInfo(res.data as User);
console.log("refresh token received", err.config, res.data);
resolve(axios(err.config));
})
.catch(() => {
console.log("refresh token ERROR");
authStore.logout();
})
.finally(() => {
authStore.isRefreshing = false;
});
});
} else {
// not the first request, wait for first request to finish
return new Promise((resolve, reject) => {
const intervalId = setInterval(() => {
console.log("refresh token - waiting");
if (!authStore.isRefreshing) {
clearInterval(intervalId);
console.log("refresh token - waiting resolved", err.config);
resolve(axios(err.config));
}
}, 100);
});
}
}
return Promise.reject(err);
}
);
But when axios retries the post request, it changes the content-type:
versus the original request (with content-type application/json)
I've read every post/example I could possible find with no luck, I am relatively new to axios and any guidance/examples/documentation is greatly appreciated, I'm against the wall.
To clarify, I used this pattern because it was the most complete example I was able to put together using many different sources, I would appreciate if someone had a better pattern.
Here's your problem...
config.headers = {
Authorization: `Bearer ${authStore.user.accessToken}`,
};
You're completely overwriting the headers object in your request interceptor, leaving it bereft of everything other than Authorization.
Because the replayed err.config has already serialised the request body into a string, removing the previously calculated content-type header means the client has to infer a plain string type.
What you should do instead is directly set the new header value without overwriting the entire object.
config.headers.Authorization = `Bearer ${authStore.user.accessToken}`;
See this answer for an approach to queuing requests behind an in-progress (re)authentication request that doesn't involve intervals or timeouts.

Angular Auth OIDC; Azue B2C : Getting user profile loading and promise uncaught error after getting Token from Azure

I have implemented auth authentication in Angular 9 app with Azure B2C with using angular-oauth2-oidc library and have successfully managed to get token however I am getting 'Uncaught (in promise)' and error 'loading user info TypeError' error the same time.
user profile error
Promise Uncaught Error
Auth Service
#Injectable()
export class AuthService implements OnInit{
_accessToken: string;
_idToken: string;
private isAuthenticatedSubject$ = new BehaviorSubject<boolean>(false);
public isAuthenticated$ = this.isAuthenticatedSubject$.asObservable();
private isDoneLoadingSubject$ = new ReplaySubject<boolean>();
public isDoneLoading$ = this.isDoneLoadingSubject$.asObservable();
public canActivateProtectedRoutes$: Observable<boolean> = combineLatest([
this.isAuthenticated$,
this.isDoneLoading$
]).pipe(map(values => values.every(b => b)));
constructor(
private oauthService: OAuthService,
private router: Router
){
//debugging - Capture Error Events
this.oauthService.events.subscribe(event =>{
if(event instanceof OAuthErrorEvent){
console.error(event);
}
else{
console.warn(event);
}
});
console.warn('Noticed changes to access_token (most likely from another tab), updating isAuthenticated');
this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken());
this.oauthService.events
.subscribe(_ => {
this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken());
});
this.oauthService.events
.pipe(filter(e => ['token_received'].includes(e.type)))
.subscribe(e => this.oauthService.loadUserProfile());
this.oauthService.events
.pipe(filter(e => ['session_terminated', 'session_error'].includes(e.type)))
.subscribe(e => this.navigateToLoginPage());
this.oauthService.setupAutomaticSilentRefresh();
}
public initializeAuthService(): Promise<void>{
this.oauthService.configure(authConfig);
if (location.hash) {
console.log('Encountered hash fragment, plotting as table...');
console.table(location.hash.substr(1).split('&').map(kvp => kvp.split('=')));
}
return this.oauthService.loadDiscoveryDocument(DiscoveryDocumentConfig.url)
.then(() => this.oauthService.tryLogin())
.then(() => {
if(this.oauthService.hasValidAccessToken()){
console.log("Discovery document resolved, Token does exist...", this.oauthService.getAccessToken());
return Promise.resolve();
}
})
.catch(error => {
console.error(error);
})
}
public login(targetUrl?: string){
this.oauthService.tryLogin({});
if(!this.oauthService.getAccessToken()){
this.oauthService.initImplicitFlow();
}
}
This sample works with IdentityServer but I am using Azure AD B2C.
I commented following code in AuthService and everything seems to be working now:
// this.oauthService.events
// .pipe(filter((e) => ["token_received"].includes(e.type)))
// .subscribe((e) => this.oauthService.loadUserProfile());

Saving Auth token in localStorage (React + ROR)

After login functionality, I am saving my auth token in browser's localStorage, so that i can authenticate each action fired to the server. After login i have to refresh my browser to retrieve my token, since root component is not rerendered. is there any way to rerender index.js? I am building an electron app, so browser refresh is not an option.
in index.js
const AUTH_TOKEN = localStorage.getItem('user')
if(AUTH_TOKEN){
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
store.dispatch({type: AUTHENTICATED});
}
but this will only get rendered only the first time app loads. Store and routes are defined inside. So after login auth token will be saved in localstorage but its not updated in app. Any ideas?
You can just set the axios authorization header after saving the auth token.
So this is what i did
In Auth action
localStorage.setItem('user', response.data.auth_token);
localStorage.setItem('name', response.data.user.name);
localStorage.setItem('email', response.data.user.email);
axios.defaults.headers.common['Authorization'] = response.data.auth_token;
dispatch({ type: AUTHENTICATED });
you can manage token like there, its updated on token expire, token change
const watchToken = (delay) => {
return setTimeout(() => {
myApi.getToken().then(res => {
if (res.token) {
store.dispatch({ type: AUTHENTICATED })
} else {
store.dispatch({ type: NOT_AUTHENTICATED })
}
})
}, delay);
}
class App extends Component {
tokenManager;
constructor() {
super();
this.state = {
isReady: false
};
}
componentWillMount() {
this.tokenManager = watchToken(20000)
}
componentWillUpdate(nextProps, nextState) {
if (nextProps.TOKEN !== this.props.TOKEN) {
this.tokenManager = watchToken(20000);
}
}
componentWillUnmount() {
this.tokenManager.clearTimeout();
}
render() {
return (
<div>
</div>
);
}
}

Angular 2 redirect if user have shop

How i can make redirect if User -id(this.authTokenService.currentUserData.id) have shop (owner_id) .
Shop.component.ts
owner_id :number;
private sub: any;
ngOnInit() {
this.sub = this.httpService.getShops().subscribe(params => {
this.owner_id = this.authTokenService.currentUserData.id ;
console.log(this.sub)
console.log(this.owner_id)
});
if (this.sub) {
this.router.navigate(['/profile']);
}
Http.service.ts
getShops(){
return this.http.get('http://localhost:3000/shops.json')
}
I use Rails 5 for api and auth token. Thanks for help. Sorry for my English
I can't complete grasp your intentions here, but this code seems to be what you're after. Hope this helps
ngOnInit() {
this.getUserShops(this.authTokenService.currentUserData.id)
.subscribe((ownerShops) => {
if (ownerShops.length > 0) {
// User has at least 1 shop
this.router.navigate(['/profile']);
} else {
// User has no shops
}
})
}
getUserShops(ownerId: number): Observable<any> {
return this.httpService
.getShops()
// Map the returned array to a filtered subset including only the owners id
.map((shops: any[]) => shops.filter(shop => shop.owner_id === ownerId));
}
// http.service.ts
export class HttpService {
getShops(): Observable<Shop[]> {
return this.http.get('http://localhost:3000/shops.json')
.map((res: Response) => res.json())
}
}
export interface Shop {
owner_id: number | null;
}
EDIT: Added update to show example http.service.ts typings
private sub: any;
ngOnInit() {
this.httpService.getShops()
.subscribe(params => {
// code here is executed when the response arrives from the server
this.owner_id = this.authTokenService.currentUserData.id ;
console.log(this.sub)
console.log(this.owner_id)
if (this.sub) {
this.router.navigate(['/profile']);
}
});
// code here is executed immediately after the request is sent
// to the server, but before the response arrives.
}
If the code depends on sub that you get from subscribe, then you need to move the code inside the subscribe callback, otherwise it will be executed before this.sub gets a value.
try this code.
owner_id :number;
private sub: any;
ngOnInit() {
this.httpService.getShops().subscribe(
response => {
this.sub = response
if (this.sub.length>0) {
this.router.navigate(['/profile']);
}
}
});

Resources