I have a site which is using IS4 and the front end is Angular 7. I have the oidc-client library to handle all user authentication etc and everything works fine. This is the first time I'm seeing this issue though. I can log in and interact just fine, with my permissions, role stuff and it's all good. Sometimes I get a strange error after some inactivity so when I try to perform a secured operation I get an error straightaway without even hitting my API. If I refresh the page and perform the action, it works fine.
I have implemented the silent renew callback in my component just like this:
#Component({
selector: 'app-silent-renew-callback',
templateUrl: 'silent-renew-callback.component.html'
})
export class SilentRenewCallbackComponent implements OnInit {
constructor(private authService: AuthService) { }
ngOnInit() {
this.authService.signingSilentCallback();
}
}
which then goes off to the authService code:
signingSilentCallback(): Promise<void> {
return this.manager.signinSilentCallback()
.catch(e => {
console.log(e);
});
}
My auth service event configuration looks like this:
#Injectable()
export class AuthService {
private manager: UserManager;
private user: User = null;
constructor() {
if (!environment.production) {
Log.logger = console;
}
this.manager = new UserManager(getClientSettings());
this.manager.getUser()
.then(user => {
this.user = user;
});
this.manager.events.addUserSignedOut(() => {
this.signOut();
});
this.manager.events.addAccessTokenExpired(() => {
this.signOut();
});
}
}
export function getClientSettings(): UserManagerSettings {
return {
authority: environment.authorityUrl,
client_id: 'my_client_id',
redirect_uri: `${environment.baseUrl}/auth-callback`,
post_logout_redirect_uri: environment.baseUrl,
response_type: 'id_token token',
scope: 'openid profile my_api',
filterProtocolClaims: true,
loadUserInfo: true,
automaticSilentRenew: true,
silent_redirect_uri: `${environment.baseUrl}/silent-renew-callback`,
};
}
The thing is that reading some posts it looks like there should be an event hooked up when you renew the user:
this.manager.events.addUserLoaded(_ => {
this.manager.getUser()
.then(user => {
this.user = user;
});
});
My question is whether that's mandatory to be added when you use the silent renew or not and also, in case that is not needed, if that rings the bell of any issues you guys have come across this before.
Thanks
Related
I have an identity server 4 implementation very simple and I'm using the oidc-client on my angular APP to carry on all the security management. In my auth service I have the following:
#Injectable()
export class AuthService {
private manager: UserManager;
private user: User = null;
constructor() {
if (!environment.production) {
Log.logger = console;
}
this.manager = new UserManager(getClientSettings());
this.manager.getUser()
.then(user => {
this.user = user;
});
this.manager.events.addUserSignedOut(() => {
this.user = null;
this.signOut();
});
}
signOut(): Promise<void> {
return this.manager.signoutRedirect()
.then(() => {
this.manager.clearStaleState();
});
}
...more
}
export function getClientSettings(): UserManagerSettings {
return {
authority: environment.authorityUrl,
client_id: 'myclient',
redirect_uri: `${environment.baseUrl}/auth-callback`,
post_logout_redirect_uri: environment.baseUrl,
response_type: 'id_token token',
scope: 'openid profile myapi',
filterProtocolClaims: true,
loadUserInfo: true,
automaticSilentRenew: true,
revokeAccessTokenOnSignout : true,
silent_redirect_uri: `${environment.baseUrl}/silent-renew-callback`,
};
}
Everything works like a charm so I can log in/out without any issues, the token renewal works as expected and so far so good. However I decided to implement a "custom" behavior when a user open the application in multiple tabs on the same browser and one of them logs out. Then the event UserSignedOut is triggered and I sign out the rest of the tabs that may be open. The problem I have is that when the user comes back in, the FIRST login attempt is performed correctly, however any subsequent login request from any other tab results in a 400 - BAD REQUEST (removing the Antiforgery token attribute from the login method in my IS4 it then works but I don't want to do so).
If you did refresh the tab then you get logged in therefore to me it seems something wrong with the actual state itself?
I'm not sure if I should invoke any other method in my AuthService prior logging out or whether I should re-implement the ValidateAntiForgeryToken with a custom behavior for this.
Any help is much appreciated, thanks!
I am kind of new to angular 2 so ill try to explain the requirement in details.
The app I build has a login page (/login) and has the settings page (/settings).
when the user access the login page the gapi var is initialized properly then the user logs in to the app.
Once the user is in he has the settings page. the issue starts when the user refresh the page, when that happens the gapi var is no longer recognized and become undefined. My though is that the gapi library is not getting loaded and therefore it fails.
I placed the following code in the app index.html file
<script type="text/javascript">
// Client ID and API key from the Developer Console
var CLIENT_ID = '***.apps.googleusercontent.com';
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = 'https://www.googleapis.com/auth/gmail.readonly';
/**
* On load, called to load the auth2 library and API client library.
*/
function handleClientLoad() {
console.log("handleClientLoad")
gapi.load('client:auth2', initClient);
}
/**
* Initializes the API client library and sets up sign-in state
* listeners.
*/
function initClient() {
gapi.client.init({
discoveryDocs: DISCOVERY_DOCS,
clientId: CLIENT_ID,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes.
console.log("client init");
gapi.auth2.getAuthInstance().isSignedIn.listen();
// Handle the initial sign-in state.
//updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
});
}
</script>
<script async defer src="https://apis.google.com/js/api.js"
onload=this.onload=function(){};handleClientLoad();
onreadystatechange="if (this.readyState === 'complete') this.onload()";>
</script>
To conclude how can I properly load the gapi module to handle the above refresh scenario?
I tried to work with the solution from Best way to wait for 3rd-party JS library to finish initializing within Angular 2 service? however it didnt work, gapi is still undefined.
What I've done is create a custom GoogleService that is responsible for initializing the GAPI client in Angular apps. Instead of interacting with the GAPI client directly, my app interacts with the GoogleService.
For example (using Angular 9.x)
import { Injectable } from '#angular/core';
import { environment } from '../environments/environment';
#Injectable({
providedIn: 'root',
})
export class GoogleService {
private gapiAuth?: Promise<gapi.auth2.GoogleAuth>;
constructor() {
// Chrome lets us load the SDK on demand, but firefox will block the popup
// when loaded on demand. If we preload in the constructor,
// then firefox won't block the popup.
this.googleSDK();
}
async signinGoogle() {
const authClient = (await this.googleSDK()) as gapi.auth2.GoogleAuth;
const googleUser = await authClient.signIn();
const profile = googleUser.getBasicProfile();
return {
type: 'GOOGLE',
token: googleUser.getAuthResponse().id_token as string,
uid: profile.getId() as string,
firstName: profile.getGivenName() as string,
lastName: profile.getFamilyName() as string,
photoUrl: profile.getImageUrl() as string,
emailAddress: profile.getEmail() as string,
};
}
async grantOfflineAccess() {
const authClient: gapi.auth2.GoogleAuth = (await this.googleSDK()) as any;
try {
const { code } = await authClient.grantOfflineAccess();
return code;
} catch (e) {
// access was denied
return null;
}
}
// annoyingly there is some sort of bug with typescript or the `gapi.auth2`
// typings that seems to prohibit awaiting a promise of type `Promise<gapi.auth2.GoogleAuth>`
// https://stackoverflow.com/questions/54299128/type-is-referenced-directly-or-indirectly-in-the-fulfillment-callback-of-its-own
private googleSDK(): Promise<unknown> {
if (this.gapiAuth) return this.gapiAuth;
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.defer = true;
script.src = 'https://apis.google.com/js/api.js?onload=gapiClientLoaded';
this.gapiAuth = new Promise<void>((res, rej) => {
(window as any)['gapiClientLoaded'] = res;
script.onerror = rej;
})
.then(() => new Promise(res => gapi.load('client:auth2', res)))
.then(() =>
gapi.client.init({
apiKey: environment.google.apiKey,
clientId: environment.google.clientId,
discoveryDocs: environment.google.discoveryDocs,
scope: environment.google.scopes.join(' '),
}),
)
.catch(err => {
console.error('there was an error initializing the client', err);
return Promise.reject(err);
})
.then(() => gapi.auth2.getAuthInstance());
document.body.appendChild(script);
return this.gapiAuth;
}
}
Hi I'm using Auth0 with Nodejs and angularjs
here is what i want to achieve
1. I want to user to signup using auth0's lock
2. as soon as user logs in a callback should be called at my nodejs server
3. after that i will get the user information and user's JWT
4. then i will redirect user to dashboard page and store the JWT in browser
What's the problem with Auth0's example
1. they provide example either for angular or nodejs standalone not the combined
2. there is combined(client server) example but that's using jade with nodejs
my code snipped
Angular snipped
var options = { auth: {
redirectUrl: 'http://localhost:3000/callback'
, responseType: 'code'
, params: {
scope: 'openid name email picture'
}
}
}
lockProvider.init({
clientID: 'cUlBNhhaIblahBlahRp6Km',
domain: 'rishabh.auth0.com',
option:options
});
node snipped
router.get('/callback',
passport.authenticate('auth0', { failureRedirect: '/url-if-something-fails' }),
function(req, res) {
console.log(req.user);
res.json({id_token:req.user});
});
Note: I've added this callbacks in auth0
http://localhost:3000/callback
but dont know why I'm facing this error for callback error when I've mentioned my redirect URL in angular side
can anyone tell me what is the problem with my code why auth0 not redirecting me to this url http://localhost:3000/callback
and the interesting thing is when i use simple lock.js instead of angular like this
<script>
var options = { auth: {
redirectUrl: 'http://localhost:3000/callback'
, responseType: 'code'
, params: {
scope: 'openid name email picture'
}
}
}
var lock = new Auth0Lock('clientID', 'rishabh.auth0.com',options);
lock.show();
</script>
then in this case my nodejs /callback route is called properly, so what I'm doing wrong with angular ?
please help
Update
this is my project structure
full code
https://github.com/LabN36/error
Config.js
var Auth0Strategy = require('passport-auth0');
var passport = require('passport');
var strategy = new Auth0Strategy({
domain: process.env.AUTH0_DOMAIN || 'rishabh.auth0.com',
clientID: process.env.AUTH0_CLIENT_ID || 'cUheWwRxm7OLdHBRzlBNvfvfvfvfvhhaI1lxRp6Km',
clientSecret: process.env.AUTH0_CLIENT_SECRET || 'e37eIZpjgBnDMBtrYMwvffvfvfvfaU4jSqt8qylZMT9Oj1EiffLGViinWQ5AiuWi1-WBwA8v3',
callbackURL: process.env.AUTH0_CALLBACK_URL || 'http://localhost:3000/callback'
}, function(accessToken, refreshToken, extraParams, profile, done) {
// accessToken is the token to call Auth0 API (not needed in the most cases)
// extraParams.id_token has the JSON Web Token
// profile has all the information from the user
console.log(extraParams.id_token);
//save user detail with token here and return token only profile
return done(null, extraParams.id_token);
});
passport.use(strategy);
// you can use this section to keep a smaller payload
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(user, done) {
done(null, user);
});
module.exports = passport;
AngularApp.js
angular.module('workApp',['auth0.lock'])
.config(function($locationProvider,lockProvider){
var options = { auth: {
// redirect:true,
responseType: 'code',
redirectUrl: 'http://localhost:3000/callback',
params: {
scope: 'openid name email picture'
}
}
}
lockProvider.init({clientID: 'cUheWwRxm7OLdHBRzlBNhhaI1lxRp6Km',domain: 'rishabh.auth0.com',
option:options
});
$locationProvider.html5Mode(true);
})
.controller('homeCtrl',function($scope,$http,$location,$window,lock){
$scope.login = function() {
// window.alert("magic")
console.log("Messed Up really")
var vm = this;
vm.lock = lock;
lock.show();
}
}).run(function(lock){
lock.interceptHash();
lock.on('authenticated', function(authResult) {
localStorage.setItem('id_token', authResult.idToken);
lock.getProfile(authResult.idToken, function(error, profile) {
if (error) {
console.log(error);
}
localStorage.setItem('profile', JSON.stringify(profile));
});
});
})
According to the screenshot the error happens because the authentication request is made with a redirect_uri of:
http://localhost:3000/
and the allowed callback URL's are:
http://localhost:3000/callback
http://35.162.118.253:3000/callback
Also based on the code you shared you're indeed setting the redirectUrl to be http://localhost:3000/callback so there may be something on the rest of the code that either causes that value to be overridden or not used at all.
If the redirectUrl is not set, Lock will use the current page so the likely culprit is that the options you set are not being used. If you still don't find the cause for this, update the question with the code associated with how Lock is shown.
Damn, the actual root cause was already shown in the code you initially provided, but only looking now at the full code made it possible for me to catch it...
You're calling lockProvider.init() with:
{ clientID: [?], domain: [?], option: options }
when it should be called with:
{ clientID: [?], domain: [?], options: options } // options instead of option
I have an angular 2 app with JWT authentication (Auth0) that after user login stores the profile and token id in the localStorage, the profile contains a "role" attribute to check if the user can access an especific page.
Everything works like a charm but if the user change the role property in the localStorage to "admin" for example and reload the app he can access pages that he is not authorized. How can I handle that?
auth.service.ts:
declare var Auth0Lock: any;
#Injectable()
export Class AuthService {
lock = new Auth0Lock(ID, domain);
user: Object;
constructor() {
this.user = JSON.parse(localStorage.getItem('profile'));
}
public login() {
// Auth0 method
this.lock.show({}, (err: string, profile: Object, token: string) => {
if (err) { console.log(err); return; }
localStorage.setItem('profile', JSON.stringify(profile));
localStorage.setItem('id_token', token);
});
}
public logout() {
localStorage.removeItem('profile');
localStorage.removeItem('id_token');
}
public loggedIn() {
return tokenNotExpired();
}
public isAdmin() {
if (this.user['role'] == 'admin') {
return true;
} else {
return false;
}
}
}
app.component.ts:
// imports, etc
export Class AppComponent {
constructor(private auth: AuthService) {}
}
app.component.html:
<nav>
<ul>
<li><a [routerLink]="['Customers']" *ngIf="auth.loggedIn()">Customers</a></li>
<li><a [routerLink]="['Admin']" *ngIf="auth.loggedIn() && auth.isAdmin()">Admin</a></li>
</ul>
</nav>
Any ideas to handle that would be appreciated
In my opinion, it's impossible to create fully secured pages/forms, when you have JavaScript front-end framework. I mean, there is always possibility to open any form, when all application is downloaded to and constructed at client side.
For me, it's important to secure back-end api, and leave "hacking" opportunities to junior developers.
If you have such requirements, try to use server side generated framework: php, asp.net mvc, jsp & etc.
UPDATE
As it came up, Angular2 has directive #CanActivate, which gives more accurate way to cancel navigaition. BUT server side authorization is CRITICAL in javascript front-end based applications.
This will help how to deal with JWT in express back-end: express-jwt
Hi I want to support both formbased authentication and http basic authentication in my app. Everything works as expected except when I use form based auth via angularjs with wrong credentials.
Instead of having my angular code handle the 401, the browser shows the BASIC auth dialog, caused by the WWW-Authenticate header.
How can I prevent that header from being added when the local strategy is used?
Or how can I support both mechanisms in a different way?
I use the following route in my express based app.
api.post('/authenticate', passport.authenticate(['local', 'basic'], { session: false }), function (req, res) {
This enables both authentication methods on that url. I repeat, when I use wrong credentials using formbased it shows me the basic auth dialog (I don't want that).
Following is how I registered the strategies.
passport.use(new BasicStrategy({ realm: 'Authentication failed. Wrong username or password.'}, verifyLocalUser));
passport.use(new LocalStrategy(verifyLocalUser));
This is how my verifyUser method looks like...
var verifyLocalUser = function (username, password, next) {
User.findOne({
username: username
}).select('fullname admin username password').exec(function (err, user) {
if (err) {
return next(err);
}
if (user && user.comparePasswords(password)) {
return next(null, user);
} else {
next(null, false, { message: 'Authentication failed. Wrong username or password.' });
}
});
}
Does anyone know how to support multiple authentication methods using passport.js?
For completeness, this is the angular code which authenticates me...
authFactory.signIn = function (username, password) {
return $http.post('/api/authenticate', {
username: username,
password: password
}).then(function (res) {
AuthToken.setToken(res.data.token);
return res.data;
}, function (res) {
console.warn(res);
});
};
instead of this:
next(null, false, { message: 'Authentication failed. Wrong username or password.' });
You can use this:
cb(new YourCustomError())
And "YourCustomError" can have a message, for me mine "YourCustomError" looks like:
class HttpError extends Error {
constructor (msg = 'Invalid Request', status = 400) {
super(msg)
this.status = status
}
}
class Forbidden extends HttpError {
constructor (msg = 'Forbidden') {
super(msg, 403)
}
}
Or probably new Error(<message>) will work properly for you, too