Closed jjgriff93 closed 5 years ago
Hey @jjgriff93, I believe this is the same (almost) issue as with the https://github.com/akveo/ngx-admin/issues/1757.
The first stop is the isRedirectResult
method, which is trying to determine whether the request is a redirect request or not. The decision is based on the presence of particular parameters in the callback url, such as access_token
or error
(https://github.com/akveo/nebular/blob/master/src/framework/auth/strategies/oauth2/oauth2-strategy.ts#L144). In your case both are not presented, thus the request is not treated as a callback and the strategy starts the authentication process all over again.
I would suggest creating a new strategy inherited from this one and re-defining a couple of checks:
access_token
to id_token
.NbAuthOAuth2Token
which also waits for access_token
here https://github.com/akveo/nebular/blob/master/src/framework/auth/services/token/token.ts#L155 (however, you may not use the getValue
method and retrieve the token from getPayload()['id_token']
.As for the error - it is hard to tell the reason. I would suggest adopting the strategy first, that may be a reason for the errors in the first place.
Hi @nnixaa , thanks very much for getting back to me and for your suggestions. I've tried to implement what you suggest but have a few validation errors cropping up (likely due to my inexperience with Angular!). I've created a class in core.module.ts
that extends NbOAuth2AuthStrategy
and redefined the redirectResults
and getValue
methods like so:
export class NbAzureADB2CAuthStrategy extends NbOAuth2AuthStrategy {
protected redirectResults = {
[NbOAuth2ResponseType.TOKEN]: () => {
return observableOf(this.route.snapshot.fragment).pipe(
map(fragment => this.parseHashAsQueryParams(fragment)),
map((params: any) => !!(params && (params.id_token || params.error))),
);
},
};
getValue(): string {
return this.token.id_token;
}
}
I'm getting an error on redirectResults
saying Property 'redirectResults' in type 'NbAzureADB2CAuthStrategy' is not assignable to the same property in base type 'NbOAuth2AuthStrategy'.
I'm getting another error on route
that says Property 'route' is private and only accessible within class 'NbOAuth2AuthStrategy'.
and a last error on token
saying Property 'token' does not exist on type 'NbAzureADB2CAuthStrategy'.
What am I doing wrong here? Really appreciate your help.
Thanks again!
Hey @jjgriff93, you should have something like this:
export class NbAzureADB2CAuthStrategy extends NbOAuth2AuthStrategy {
protected redirectResults = {
[NbOAuth2ResponseType.CODE]: this.redirectResults[NbOAuth2ResponseType.CODE],
[NbOAuth2ResponseType.TOKEN]: () => {
return observableOf(this.activatedRoute.snapshot.fragment).pipe(
map(fragment => this.parseHashAsQueryParams(fragment)),
map((params: any) => !!(params && (params.id_token || params.error))),
);
},
};
constructor(http: HttpClient,
private activatedRoute: ActivatedRoute,
window: any) {
super(http, activatedRoute, window);
}
}
redirectResults
(fixes the first TS error)activatedRoute
name (name conflict with a private field from the parent class) and also other contructor services are injected and passed to the parent (fixes the second error)getValue
method to a new TOKEN class, not a strategy. So simply create a custom token class (say NbAuthAzureToken
) inherited from NbAuthOAuth2Token
(https://github.com/akveo/nebular/blob/master/src/framework/auth/services/token/token.ts#L141) (third error) and overload the getValue()
method.Also please make sure you marked NbAzureADB2CAuthStrategy
as @Injecteble()
and registered it within providers: []
array of your module. At the same time the token class is not a service in Angular terms and there is no need to register it, just pass that new class name into the strategy configuration { token: { class: NbAuthAzureToken }}
Let me know how it goes.
Thanks @nnixaa, really appreciate your help. I've done as you suggested above and that's resolved all the validation errors (thank you!) - just getting a js console error now on compilation:
Uncaught Error: Can't resolve all parameters for NbAzureADB2CAuthStrategy: ([object Object], [object Object], ?). at syntaxError (compiler.js:215) at CompileMetadataResolver.push../node_modules/@angular/compiler/fesm5/compiler.js.CompileMetadataResolver._getDependenciesMetadata (compiler.js:10810)
My core.module.ts
is looking like this (with the top portion & imports cut out):
// Create new token for Azure auth so it returns id_token instead of access_token
export class NbAuthAzureToken extends NbAuthOAuth2Token {
getValue(): string {
return this.token.id_token;
}
}
@Injectable()
export class NbAzureADB2CAuthStrategy extends NbOAuth2AuthStrategy {
protected redirectResults = {
[NbOAuth2ResponseType.CODE]: this.redirectResults[NbOAuth2ResponseType.CODE],
[NbOAuth2ResponseType.TOKEN]: () => {
return observableOf(this.activatedRoute.snapshot.fragment).pipe(
map(fragment => this.parseHashAsQueryParams(fragment)),
map((params: any) => !!(params && (params.id_token || params.error))),
);
},
};
constructor(http: HttpClient,
private activatedRoute: ActivatedRoute,
window: any) {
super(http, activatedRoute, window);
}
}
export const NB_CORE_PROVIDERS = [
...DataModule.forRoot().providers,
...NbAuthModule.forRoot({
strategies: [
NbAzureADB2CAuthStrategy.setup({
name: 'AzureADB2C',
clientId: '*********************************',
clientSecret: '',
authorize: {
// tslint:disable-next-line:max-line-length
endpoint: 'https://login.microsoftonline.com/tfp/********************/B2C_signin/oauth2/v2.0/authorize',
responseType: NbOAuth2ResponseType.TOKEN, // have overloaded this to return id_token instead of token for Azure auth
scope: 'https://********.onmicrosoft.com/api/profile openid profile',
redirectUri: 'https://localhost:5001/auth/callback',
},
token: {
endpoint: 'token', // reverted from id_token
class: NbAuthAzureToken, // changed from NbAuthOAuth2Token
},
redirect: {
success: '/dashboard/overview',
},
}),
],
forms: {
login: {
socialLinks: socialLinks,
},
register: {
socialLinks: socialLinks,
},
},
}).providers,
NbSecurityModule.forRoot({
accessControl: {
guest: {
view: '*',
},
user: {
parent: 'guest',
create: '*',
edit: '*',
remove: '*',
},
},
}).providers,
{
provide: NbRoleProvider, useClass: NbSimpleRoleProvider,
},
AppMonitoringService,
];
@NgModule({
imports: [
CommonModule,
],
exports: [
NbAuthModule,
],
declarations: [],
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
static forRoot(): ModuleWithProviders {
return <ModuleWithProviders>{
ngModule: CoreModule,
providers: [
...NB_CORE_PROVIDERS,
NbAzureADB2CAuthStrategy,
],
};
}
}
Any ideas? Probably something small I've missed. Feel like it's close now! :)
Thanks again
Right, my example is missing the correct inject of the window
property. Your constructor should be like this:
constructor(http: HttpClient,
private activatedRoute: ActivatedRoute,
@Inject(NB_WINDOW) window: any) {
super(http, activatedRoute, window);
}
This should fix the error.
Hey @nnixaa, that's fixed the error thank you, so now all compiles ok - however unfortunately the original problem has returned, it's still continually redirecting after trying to authenticate, still doesn't seem to be picking up the token out of the callback URL payload and converting it into a token object. It gets to the callback component, then after a second or two goes back to another authentication attempt.
Any ideas?
Full core.module.ts
code now looks like this:
import { ModuleWithProviders, NgModule, Optional, SkipSelf, Injectable, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NbOAuth2AuthStrategy, NbAuthModule, NbOAuth2ResponseType, NbAuthOAuth2Token } from '@nebular/auth';
import { NbSecurityModule, NbRoleProvider } from '@nebular/security';
import { of as observableOf } from 'rxjs';
import { throwIfAlreadyLoaded } from './module-import-guard';
import { DataModule } from './data/data.module';
import { AppMonitoringService } from './monitoring/app-monitoring.service';
import { map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';
import { NB_WINDOW } from '@nebular/theme';
const socialLinks = [
{
url: 'https://www.facebook.com/',
target: '_blank',
icon: 'socicon-facebook',
},
];
export class NbSimpleRoleProvider extends NbRoleProvider {
getRole() {
// here you could provide any role based on any auth flow
return observableOf('guest');
}
}
// Override 'token' to 'id_token' for Azure AD B2C
(NbOAuth2ResponseType as any)['TOKEN'] = 'id_token';
// Create new token for Azure auth so it returns id_token instead of access_token
export class NbAuthAzureToken extends NbAuthOAuth2Token {
getValue(): string {
return this.token.id_token;
}
}
@Injectable()
export class NbAzureADB2CAuthStrategy extends NbOAuth2AuthStrategy {
protected redirectResults = {
[NbOAuth2ResponseType.CODE]: this.redirectResults[NbOAuth2ResponseType.CODE],
[NbOAuth2ResponseType.TOKEN]: () => {
return observableOf(this.activatedRoute.snapshot.fragment).pipe(
map(fragment => this.parseHashAsQueryParams(fragment)),
map((params: any) => !!(params && (params.id_token || params.error))),
);
},
};
constructor(http: HttpClient,
private activatedRoute: ActivatedRoute,
@Inject(NB_WINDOW) window: any) {
super(http, activatedRoute, window);
};
}
export const NB_CORE_PROVIDERS = [
...DataModule.forRoot().providers,
...NbAuthModule.forRoot({
strategies: [
NbAzureADB2CAuthStrategy.setup({
name: 'AzureADB2C',
clientId: '************************************',
clientSecret: '',
authorize: {
// tslint:disable-next-line:max-line-length
endpoint: 'https://login.microsoftonline.com/tfp/*******.onmicrosoft.com/******/oauth2/v2.0/authorize',
responseType: NbOAuth2ResponseType.TOKEN, // have overloaded this to return id_token instead of token for Azure auth
scope: 'https://******.onmicrosoft.com/api/profile openid profile',
redirectUri: 'https://localhost:5001/auth/callback',
},
token: {
class: NbAuthAzureToken,
},
redirect: {
success: '/dashboard/overview',
},
}),
],
forms: {
login: {
socialLinks: socialLinks,
},
register: {
socialLinks: socialLinks,
},
},
}).providers,
NbSecurityModule.forRoot({
accessControl: {
guest: {
view: '*',
},
user: {
parent: 'guest',
create: '*',
edit: '*',
remove: '*',
},
},
}).providers,
{
provide: NbRoleProvider, useClass: NbSimpleRoleProvider,
},
AppMonitoringService,
];
@NgModule({
imports: [
CommonModule,
],
exports: [
NbAuthModule,
],
declarations: [],
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
static forRoot(): ModuleWithProviders {
return <ModuleWithProviders>{
ngModule: CoreModule,
providers: [
...NB_CORE_PROVIDERS,
NbAzureADB2CAuthStrategy,
],
};
}
}
Hey @jjgriff93, hard to tell, have you tried to debug it? For example, what would be the result if you print it from this line https://github.com/akveo/nebular/blob/2.0.0-rc.9/src/framework/auth/strategies/oauth2/oauth2-strategy.ts#L161?
Hi @nnixaa - yes, have stepped through what happens during the auth circling and have done so with the breakpoint you've suggested - the local variable result
is false
which explains why it's continually retrying. The variables when the breakpoint is hit are as follows:
1. Local
1. result:false
2. this:SwitchMapSubscriber
2. Closure
3. (push../node_modules/@nebular/auth/strategies/oauth2/oauth2-strategy.js.NbOAuth2AuthStrategy.authenticate)
1. _this:NbOAuth2AuthStrategy
1. defaultOptions:NbOAuth2AuthStrategyOptions
1. authorize:
1. endpoint:"authorize"
2. responseType:"code"
3. __proto__:Object
2. baseEndpoint:""
3. clientId:""
4. clientSecret:""
5. defaultErrors:["Something went wrong, please try again."]
6. defaultMessages:["You have been successfully authenticated."]
7. redirect:{success: "/", failure: null}
8. refresh:{endpoint: "token", grantType: "refresh_token"}
9. token:{endpoint: "token", grantType: "authorization_code", class: ƒ}
10. __proto__:Object
2. http:HttpClient {handler: HttpInterceptingHandler}
3. options:
1. authorize:
1. endpoint:"https://login.microsoftonline.com/tfp/pixeldr.onmicrosoft.com/b2c_1_pxdrsignin/oauth2/v2.0/authorize"
2. redirectUri:"https://localhost:5001/auth/callback"
3. responseType:"id_token"
4. scope:"https://PixelDr.onmicrosoft.com/api/profile openid profile"
5. __proto__:Object
2. baseEndpoint:""
3. clientId:"7f1d19f9-80ed-43c8-86df-ed78b2bd6275"
4. clientSecret:""
5. defaultErrors:["Something went wrong, please try again."]
6. defaultMessages:["You have been successfully authenticated."]
7. name:"AzureADB2C"
8. redirect:{success: "/dashboard/overview", failure: null}
9. refresh:{endpoint: "token", grantType: "refresh_token"}
10. token:{endpoint: "token", grantType: "authorization_code", class: ƒ}
11. __proto__:Object
4. redirectResultHandlers:
1. code:ƒ ()
2. id_token:ƒ ()
3. __proto__:Object
5. redirectResults:{code: ƒ, id_token: ƒ}
6. responseType:(...)
7. route:ActivatedRoute
1. children:(...)
2. component:ƒ AppComponent(appMonitoringService)
3. data:BehaviorSubject {_isScalar: false, observers: Array(0), closed: false, isStopped: false, hasError: false, …}
4. firstChild:(...)
5. fragment:BehaviorSubject
1. closed:false
2. hasError:false
3. isStopped:false
4. observers:[]
5. thrownError:null
6. value:(...)
7. _isScalar:false
8. _value:"id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1jNTdkTzZRR1RWQndhTmsifQ.eyJleHAiOjE1MzE2ODY1NjMsIm5iZiI6MTUzMTY4Mjk2MywidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tL2M0YWI4YzIxLWRhODItNDI5NS05ODMyLTViODBhZDllM2JhYi92Mi4wLyIsInN1YiI6ImFlZDQ3NmVkLTc3OTAtNDVhOS05MDkyLTQzN2RlZjIzNTkzYSIsImF1ZCI6IjdmMWQxOWY5LTgwZWQtNDNjOC04NmRmLWVkNzhiMmJkNjI3NSIsImlhdCI6MTUzMTY4Mjk2MywiYXV0aF90aW1lIjoxNTMxNjgyOTYzLCJvaWQiOiJhZWQ0NzZlZC03NzkwLTQ1YTktOTA5Mi00MzdkZWYyMzU5M2EiLCJnaXZlbl9uYW1lIjoiSmFtZWQiLCJmYW1pbHlfbmFtZSI6IkdyaWZmIiwibmFtZSI6IkdyaW1ibGU2OSIsImVtYWlscyI6WyJqamdyaWZmOTNAaG90bWFpbC5jby51ayJdLCJ0ZnAiOiJiMmNfMV9weGRyc2lnbmluIn0.Mn9E2vHuQT6ga_yJ7OwoX91Vxe_rGJ4cRmNFnC6YzE477Qy2JjvntmTysttt8-0IF4xHTYnqG-xoJQ3JpiK7gxTJLePwasIJJpDxRN4NJfGQY4b6qPAR58XVDX0RlPiK40ApOhxdu6q91zcSIdzAjunfj-n7O5vNpXGPCBY_CdyYz4ueDooWT2Bgq6VO3V9jjOsWwi260fLvJ3JNndoMwcyrN5w1zVBMolXYnwe7VzVSs8Qztdo09b9CBHBStwj0yUMAmlwDMMCylGed6-UeHS2P_-Cmmqk6rtw6yTzd7-TSpjj_HuWlhmWTSip8FPMOlZma1dVmQpFpwh782dG1eA"
9. __proto__:Subject
6. outlet:"primary"
7. paramMap:(...)
8. params:BehaviorSubject {_isScalar: false, observers: Array(0), closed: false, isStopped: false, hasError: false, …}
9. parent:(...)
10. pathFromRoot:(...)
11. queryParamMap:(...)
12. queryParams:BehaviorSubject {_isScalar: false, observers: Array(0), closed: false, isStopped: false, hasError: false, …}
13. root:(...)
14. routeConfig:(...)
15. snapshot:ActivatedRouteSnapshot {url: Array(0), params: {…}, queryParams: {…}, fragment: "id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZ…Tzd7-TSpjj_HuWlhmWTSip8FPMOlZma1dVmQpFpwh782dG1eA", data: {…}, …}
16. url:BehaviorSubject {_isScalar: false, observers: Array(0), closed: false, isStopped: false, hasError: false, …}
17. _futureSnapshot:ActivatedRouteSnapshot {url: Array(0), params: {…}, queryParams: {…}, fragment: "id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZ…Tzd7-TSpjj_HuWlhmWTSip8FPMOlZma1dVmQpFpwh782dG1eA", data: {…}, …}
18. _routerState:RouterState {_root: TreeNode, snapshot: RouterStateSnapshot}
19. __proto__:Object
8. window:Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
__proto__:NbAuthStrategy
It looks like the token endpoint is still showing as token
and not id_token
, could this be what's causing it to not pick up the token properly? If so how would I go about resolving this?
Thanks again for your help and sorry for the delay!
@jjgriff93 here you go:
// Override 'token' to 'id_token' for Azure AD B2C
(NbOAuth2ResponseType as any)['TOKEN'] = 'id_token';
// Create new token for Azure auth so it returns id_token instead of access_token
export class NbAuthAzureToken extends NbAuthOAuth2Token {
// let's rename it to exclude name clashes
static NAME = 'nb:auth:azure:token';
getValue(): string {
return this.token.id_token;
}
}
@Injectable()
export class NbAzureADB2CAuthStrategy extends NbOAuth2AuthStrategy {
// we need this methos for strategy setup
static setup(options: NbOAuth2AuthStrategyOptions): [NbAuthStrategyClass, NbOAuth2AuthStrategyOptions] {
return [NbAzureADB2CAuthStrategy, options];
}
protected redirectResults: any = {
[NbOAuth2ResponseType.CODE]: () => observableOf(null),
[NbOAuth2ResponseType.TOKEN]: () => {
// queryParams here as token returned not as hash
return observableOf(this.activatedRoute.snapshot.queryParams).pipe(
map((params: any) => {
return !!(params && (params.id_token || params.error));
}),
);
},
};
protected redirectResultHandlers = {
[NbOAuth2ResponseType.CODE]: () => observableOf(null),
[NbOAuth2ResponseType.TOKEN]: () => {
// same here, token isn't in hash so have to redefine this to work with queryParams
return observableOf(this.activatedRoute.snapshot.queryParams).pipe(
map((params: any) => {
if (!params.error) {
return new NbAuthResult(
true,
params,
this.getOption('redirect.success'),
[],
this.getOption('defaultMessages'),
this.createToken(params));
}
return new NbAuthResult(
false,
params,
this.getOption('redirect.failure'),
this.getOption('defaultErrors'),
[],
);
}),
);
},
};
constructor(http: HttpClient,
private activatedRoute: ActivatedRoute,
@Inject(NB_WINDOW) window: any) {
super(http, activatedRoute, window);
}
}
A couple of re-doings and now it is working. I added a couple of comments with the changes.
Hey @nnixaa , thanks very much for putting this together - took me a while to test as I've been away for a few days. Unfortunately still getting result: false
returned after implementing the above in the code, not sure why.
Here's the full .ts file with your modified section included:
import { ModuleWithProviders, NgModule, Optional, SkipSelf, Injectable, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
// tslint:disable-next-line:max-line-length
import { NbOAuth2AuthStrategy, NbAuthModule, NbOAuth2ResponseType, NbAuthOAuth2Token, NbAuthResult, NbOAuth2AuthStrategyOptions, NbAuthStrategyClass } from '@nebular/auth';
import { NbSecurityModule, NbRoleProvider } from '@nebular/security';
import { of as observableOf } from 'rxjs';
import { throwIfAlreadyLoaded } from './module-import-guard';
import { DataModule } from './data/data.module';
import { AppMonitoringService } from './monitoring/app-monitoring.service';
import { map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';
import { NB_WINDOW } from '@nebular/theme';
const socialLinks = [
{
url: 'https://www.facebook.com/************/',
target: '_blank',
icon: 'socicon-facebook',
},
];
export class NbSimpleRoleProvider extends NbRoleProvider {
getRole() {
// here you could provide any role based on any auth flow
return observableOf('guest');
}
}
// Override 'token' to 'id_token' for Azure AD B2C
(NbOAuth2ResponseType as any)['TOKEN'] = 'id_token';
// Create new token for Azure auth so it returns id_token instead of access_token
export class NbAuthAzureToken extends NbAuthOAuth2Token {
// let's rename it to exclude name clashes
static NAME = 'nb:auth:azure:token';
getValue(): string {
return this.token.id_token;
}
}
@Injectable()
export class NbAzureADB2CAuthStrategy extends NbOAuth2AuthStrategy {
// we need this methos for strategy setup
static setup(options: NbOAuth2AuthStrategyOptions): [NbAuthStrategyClass, NbOAuth2AuthStrategyOptions] {
return [NbAzureADB2CAuthStrategy, options];
}
protected redirectResults: any = {
[NbOAuth2ResponseType.CODE]: () => observableOf(null),
[NbOAuth2ResponseType.TOKEN]: () => {
// queryParams here as token returned not as hash
return observableOf(this.activatedRoute.snapshot.queryParams).pipe(
map((params: any) => {
return !!(params && (params.id_token || params.error));
}),
);
},
};
protected redirectResultHandlers = {
[NbOAuth2ResponseType.CODE]: () => observableOf(null),
[NbOAuth2ResponseType.TOKEN]: () => {
// same here, token isn't in hash so have to redefine this to work with queryParams
return observableOf(this.activatedRoute.snapshot.queryParams).pipe(
map((params: any) => {
if (!params.error) {
return new NbAuthResult(
true,
params,
this.getOption('redirect.success'),
[],
this.getOption('defaultMessages'),
this.createToken(params));
}
return new NbAuthResult(
false,
params,
this.getOption('redirect.failure'),
this.getOption('defaultErrors'),
[],
);
}),
);
},
};
constructor(http: HttpClient,
private activatedRoute: ActivatedRoute,
@Inject(NB_WINDOW) window: any) {
super(http, activatedRoute, window);
}
}
export const NB_CORE_PROVIDERS = [
...DataModule.forRoot().providers,
...NbAuthModule.forRoot({
strategies: [
NbAzureADB2CAuthStrategy.setup({
name: 'AzureADB2C',
clientId: '******************************',
clientSecret: '',
authorize: {
// tslint:disable-next-line:max-line-length
endpoint: 'https://login.microsoftonline.com/tfp/******.onmicrosoft.com/b2c_1_pxdrsignin/oauth2/v2.0/authorize',
responseType: NbOAuth2ResponseType.TOKEN, // have overloaded this to return id_token instead of token for Azure auth
scope: 'https://******.onmicrosoft.com/api/profile openid profile',
redirectUri: 'https://localhost:5001/auth/callback',
},
token: {
class: NbAuthAzureToken, // changed from NbAuthOAuth2Token
},
redirect: {
success: '/dashboard/overview',
},
}),
],
forms: {
login: {
socialLinks: socialLinks,
},
register: {
socialLinks: socialLinks,
},
},
}).providers,
NbSecurityModule.forRoot({
accessControl: {
guest: {
view: '*',
},
user: {
parent: 'guest',
create: '*',
edit: '*',
remove: '*',
},
},
}).providers,
{
provide: NbRoleProvider, useClass: NbSimpleRoleProvider,
},
AppMonitoringService,
];
@NgModule({
imports: [
CommonModule,
],
exports: [
NbAuthModule,
],
declarations: [],
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
static forRoot(): ModuleWithProviders {
return <ModuleWithProviders>{
ngModule: CoreModule,
providers: [
...NB_CORE_PROVIDERS,
NbAzureADB2CAuthStrategy,
],
};
}
}
May be something I've done wrong on either side of the section? This is the trace I'm now getting when the breakpoint in oauth2-strategy.js
is hit (line 139):
1. Local
1. result:false
2. this:SwitchMapSubscriber
2. Closure (push../node_modules/@nebular/auth/strategies/oauth2/oauth2-strategy.js.NbOAuth2AuthStrategy.authenticate)
1. _this:NbAzureADB2CAuthStrategy
1. activatedRoute:ActivatedRoute {url: BehaviorSubject, params: BehaviorSubject, queryParams: BehaviorSubject, fragment: BehaviorSubject, data: BehaviorSubject, …}
2. defaultOptions:NbOAuth2AuthStrategyOptions
1. authorize:{endpoint: "authorize", responseType: "code"}
2. baseEndpoint:""
3. clientId:""
4. clientSecret:""
5. defaultErrors:["Something went wrong, please try again."]
6. defaultMessages:["You have been successfully authenticated."]
7. redirect:{success: "/", failure: null}
8. refresh:{endpoint: "token", grantType: "refresh_token"}
9. token:{endpoint: "token", grantType: "authorization_code", class: ƒ}
10. __proto__:Object
3. http:HttpClient {handler: HttpInterceptingHandler}
4. options:
1. authorize:{endpoint: "https://login.microsoftonline.com/tfp/******.onmicrosoft.com/b2c_1_pxdrsignin/oauth2/v2.0/authorize", responseType: "id_token", scope: "https://******.onmicrosoft.com/api/profile openid profile", redirectUri: "https://localhost:5001/auth/callback"}
2. baseEndpoint:""
3. clientId:"*********************************"
4. clientSecret:""
5. defaultErrors:["Something went wrong, please try again."]
6. defaultMessages:["You have been successfully authenticated."]
7. name:"AzureADB2C"
8. redirect:{success: "/dashboard/overview", failure: null}
9. refresh:{endpoint: "token", grantType: "refresh_token"}
10. token:{endpoint: "token", grantType: "authorization_code", class: ƒ}
11. __proto__:Object
5. redirectResultHandlers:{code: ƒ, id_token: ƒ}
6. redirectResults:{code: ƒ, id_token: ƒ}
7. responseType:(...)
8. route:ActivatedRoute {url: BehaviorSubject, params: BehaviorSubject, queryParams: BehaviorSubject, fragment: BehaviorSubject, data: BehaviorSubject, …}
9. window:Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
10. __proto__:NbOAuth2AuthStrategy
3. Closure
1. NbOAuth2AuthStrategy:ƒ NbOAuth2AuthStrategy(http, route, window)
2. _super:ƒ NbAuthStrategy()
4. Closure (./node_modules/@nebular/auth/strategies/oauth2/oauth2-strategy.js)
5. Window
6. Global
Any ideas? Thanks again for your ongoing support with this
Hi @nnixaa any thoughts on what could be missing? Struggling to find anything that could still be causing the redirect
Hey @jjgriff93, I guess your router setting useHash
is set to false, isn't it? I most likely missed that part modifying that example. This one works fine on my side (note I have to remove some of imported files as I don't have those):
import { ModuleWithProviders, NgModule, Optional, SkipSelf, Injectable, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
// tslint:disable-next-line:max-line-length
import { NbOAuth2AuthStrategy, NbAuthModule, NbOAuth2ResponseType, NbAuthOAuth2Token, NbAuthResult, NbOAuth2AuthStrategyOptions, NbAuthStrategyClass } from '@nebular/auth';
import { NbSecurityModule, NbRoleProvider } from '@nebular/security';
import { of as observableOf } from 'rxjs';
import { throwIfAlreadyLoaded } from './module-import-guard';
import { DataModule } from './data/data.module';
import { map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';
import { NB_WINDOW } from '@nebular/theme';
import { AnalyticsService } from './utils/analytics.service';
import { I18nService } from './data/i18n.service';
const socialLinks = [
{
url: 'https://www.facebook.com/************/',
target: '_blank',
icon: 'socicon-facebook',
},
];
export class NbSimpleRoleProvider extends NbRoleProvider {
getRole() {
// here you could provide any role based on any auth flow
return observableOf('guest');
}
}
// Override 'token' to 'id_token' for Azure AD B2C
(NbOAuth2ResponseType as any)['TOKEN'] = 'id_token';
// Create new token for Azure auth so it returns id_token instead of access_token
export class NbAuthAzureToken extends NbAuthOAuth2Token {
// let's rename it to exclude name clashes
static NAME = 'nb:auth:azure:token';
getValue(): string {
return this.token.id_token;
}
}
@Injectable()
export class NbAzureADB2CAuthStrategy extends NbOAuth2AuthStrategy {
// we need this methos for strategy setup
static setup(options: NbOAuth2AuthStrategyOptions): [NbAuthStrategyClass, NbOAuth2AuthStrategyOptions] {
return [NbAzureADB2CAuthStrategy, options];
}
protected redirectResults: any = {
[NbOAuth2ResponseType.CODE]: () => observableOf(null),
[NbOAuth2ResponseType.TOKEN]: () => {
return observableOf(this.activatedRoute.snapshot.fragment).pipe(
map(fragment => this.parseHashAsQueryParams(fragment)),
map((params: any) => !!(params && (params.id_token || params.error))),
);
},
};
constructor(http: HttpClient,
private activatedRoute: ActivatedRoute,
@Inject(NB_WINDOW) window: any) {
super(http, activatedRoute, window);
}
}
export const NB_CORE_PROVIDERS = [
...DataModule.forRoot().providers,
...NbAuthModule.forRoot({
strategies: [
NbAzureADB2CAuthStrategy.setup({
name: 'AzureADB2C',
clientId: '******************************',
clientSecret: '',
authorize: {
// tslint:disable-next-line:max-line-length
endpoint: 'https://login.microsoftonline.com/tfp/******.onmicrosoft.com/b2c_1_pxdrsignin/oauth2/v2.0/authorize',
responseType: NbOAuth2ResponseType.TOKEN, // have overloaded this to return id_token instead of token for Azure auth
scope: 'https://******.onmicrosoft.com/api/profile openid profile',
redirectUri: 'https://localhost:5001/auth/callback',
},
token: {
class: NbAuthAzureToken, // changed from NbAuthOAuth2Token
},
redirect: {
success: '/dashboard/overview',
},
}),
],
forms: {
login: {
socialLinks: socialLinks,
},
register: {
socialLinks: socialLinks,
},
},
}).providers,
NbSecurityModule.forRoot({
accessControl: {
guest: {
view: '*',
},
user: {
parent: 'guest',
create: '*',
edit: '*',
remove: '*',
},
},
}).providers,
{
provide: NbRoleProvider, useClass: NbSimpleRoleProvider,
},
AnalyticsService,
I18nService,
];
@NgModule({
imports: [
CommonModule,
],
exports: [
NbAuthModule,
],
declarations: [],
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
static forRoot(): ModuleWithProviders {
return <ModuleWithProviders>{
ngModule: CoreModule,
providers: [
...NB_CORE_PROVIDERS,
NbAzureADB2CAuthStrategy,
],
};
}
}
To sum up, the main issue is "where to look for the token":
return observableOf(this.activatedRoute.snapshot.fragment).pipe(
As some of the backend services return it as url hash
(fragment), some as query params
, so we need to be very careful with this and somehow take this into account inside of the strategy itself.
Hi @nnixaa - thank you, after comparing the above I noticed for some reason that I was missing map(fragment => this.parseHashAsQueryParams(fragment)),
which looks like it was the key ingredient - now working :) I'll do some tests over the next few days and integrate identity properly to see if all of the values are parsed properly, but it's not continually redirecting anymore which is great news.
Can't thank you enough for your help! :)
Issue type
I'm submitting a ... (check one with "x")
Issue description
Current behavior: I've followed the documentation for setting up OAuth authentication (which I've done in the ngx-admin starter app) - instead of Google however I'm setting up Azure AD B2C authentication.
The login request works fine, it goes to Microsoft's auth servers and then returns with an
id_token
in the callback url. However, when the callback happens and the router directs to thenb-oauth2-callback.component.ts
, the 'Authenticating...' message shows up briefly, and then instead of redirecting to the homescreen as I've defined it to redirect to (as per the docs), it instead goes through the login process again, repeating the process in a never-ending loop.The url of the callback from the Microsoft auth server looks like this:
https://localhost:5001/auth/callback#id_token=eyJ0eXAiOiJKV1Q-REST-OF-TOKEN
I've tried to do some debugging, and have noticed a couple of things when going through the code -
There's an exception in the
token-parceler.js
file:Cannot read property 'name' of null at NbAuthTokenParceler.push...
And a 'no match' exception in the
router.js
file with the url parameter in23. this.Recognizer
showing as 'auth/callback#id_tokeneyJ0eX-REST-OF-TOKEN'.Not sure if this is relevant to the redirect looping issue however seems like the token isn't being recognised/intercepted from the url header and that the router is treating it as a url fragment.
Really appreciate any help in figuring this out!
Expected behavior:
Steps to reproduce: In the ngx-admin starter app, I've created a
nb-oauth2-login.component.ts
like so:And a
nb-oauth2-callback.component.ts
like so:Set up my
core.module.ts
file with an authentication strategy like so:And finally modified the
app-routing.module.ts
like so:This is all using the latest Nebular release.
Thanks again in advance for any help!