How do I pass dynamic param in nestjs facebook strategy callback url - facebook

How do i pass some dynamic params in the facebook login callback url?
I have different types of users (differentiated by a 'type' param) signing up using facebook login. I have created a facebook auth strategy using passport-facebook which works fine.
However after authentication, when callback url is called, i need to know which type of user requested the signup.
I'm guessing i can pass a param when defining the callback url
something like this
http://localhost:3000/auth/facebook/callback/type1
http://localhost:3000/auth/facebook/callback/type2
How do I pass a dynamic value into the FacebookStrategy??
or whats the possible workaround to achieve this?
// PassportStrategy.ts
#Injectable()
export class FacebookStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
clientID: 'MYID',
clientSecret: 'MYSCRET',
callbackURL: "http://localhost:3000/auth/facebook/callback",
profileFields: ['id', 'displayName', 'emails', 'photos']
});
}
async validate(accessToken: any, refreshToken: any, profile: any) {
return {
name: profile.displayName,
email: profile.emails[0].value,
provider: "facebook",
providerId: profile.id,
photo: profile.photos[0].value
}
}
}
// auth controller
#Controller('auth')
export class AuthController {
constructor(
#Inject(forwardRef(() => AuthService)) private readonly authService: AuthService,
) { }
#Get('/facebook')
#UseGuards(AuthGuard('facebook'))
async facebookAuth(#Request() req) {
return
}
#UseGuards(AuthGuard('facebook'))
#Get('/facebook/callback')
async facebookCallback(#Request() req) {
return this.authService.login(req.user);
}
}
Basically i want to be able to call "/auth/facebook/:type" and pass the type value in the callback url defined in the Strategy
and callback endpoint to be something like "/auth/facebook/callback/:type"
so when i call the authservice.login function i can pass that 'type' and decide which type of user to be created if its the first time signup
Guide me if my approach is wrong. Thanks

I have been dealing recently with a similar issue here is my approach. Probably is not the best but works for now.
import { Inject, Injectable, Logger } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
import passport = require('passport');
import { Strategy } from 'passport-facebook';
#Injectable()
export class FacebookStrategy extends PassportStrategy(Strategy, 'facebook') {
private readonly logger = new Logger(FacebookStrategy.name);
constructor(
#Inject('FACEBOOK_STRATEGY_CONFIG')
private readonly facebookStrategyConfig,
) {
super(
facebookStrategyConfig,
async (
request: any,
accessToken: string,
refreshToken: string,
profile: any,
done,
) => {
this.logger.log(profile);
// take the state from the request query params
const { state } = request.query;
this.logger.log(state);
// register user
// return callback
return done(null, profile);
},
);
passport.use(this);
}
}
import { Controller, Get, HttpStatus, Inject, Param, Query, Req } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import { Redirect } from '#nestjsplus/redirect';
#Controller('auth')
export class AuthController {
#Inject('ConfigService')
private readonly configService: ConfigService;
#Get(':provider/callback')
#Redirect()
async socialCallback(#Req() req, #Param('provider') provider: string, #Query('state') state: string) {
// here you can use the provider and the state
return {
statusCode: HttpStatus.FOUND,
url: `${this.configService.get('FRONTEND_HOST')}/dashboard`,
};
}
}
import { MiddlewareConsumer, Module, NestModule } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import { AuthController } from './auth.controller';
import { FacebookStrategy } from './facebook.strategy';
import passport = require('passport');
const facebookStrategyConfigFactory = {
provide: 'FACEBOOK_STRATEGY_CONFIG',
useFactory: (configService: ConfigService) => {
return {
clientID: `${configService.get('FACEBOOK_CLIENT_ID')}`,
clientSecret: `${configService.get('FACEBOOK_CLIENT_SECRET')}`,
callbackURL: `${configService.get('FACEBOOK_OAUTH_REDIRECT_URI')}/callback`,
profileFields: ['id', 'displayName', 'link', 'photos', 'emails', 'name'],
passReqToCallback: true,
};
},
inject: [ConfigService],
};
#Module({
controllers: [AuthController],
providers: [facebookStrategyConfigFactory, FacebookStrategy],
})
export class AuthModule implements NestModule {
public configure(consumer: MiddlewareConsumer) {
const facebookLoginOptions = {
session: false,
scope: ['email'],
state: null,
};
consumer
.apply((req: any, res: any, next: () => void) => {
const {
query: { state },
} = req;
facebookLoginOptions.state = state;
next();
}, passport.authenticate('facebook', facebookLoginOptions))
.forRoutes('auth/facebook/*');
}
}
Now let me explain a little bit :D. The trick is in the middleware configuration.
const facebookLoginOptions = {
session: false,
scope: ['email'],
state: null,
};
consumer
.apply((req: any, res: any, next: () => void) => {
const {
query: { state },
} = req;
facebookLoginOptions.state = state;
next();
}, passport.authenticate('facebook', facebookLoginOptions))
.forRoutes('auth/facebook/*');
So, oAuth has this feature that you can pass a state param through the login flow.
By extracting the passport option in a variable we can change the state param dynamically by applying another middleware before the passport one.
In this way, you can call now http://localhost:3000/auth/facebook/login?state=anything-you-want
and this state query param will be passed through the strategy and also in the callback call.
I have also created a git repo with the example: https://github.com/lupu60/passport-dynamic-state

Another approach: the need was to dynamically set server url. It gets it using Context/Request.
// Custom Guard:
export const DynamicAuthGuard = (type?: string): Type<IAuthGuard> => {
const endpoint = `auth/${type}/redirect`
return class extends AuthGuard(type) {
getAuthenticateOptions(context: ExecutionContext) {
const httpContext: HttpArgumentsHost = context.switchToHttp()
const req: Request = httpContext.getRequest<Request>()
const serverURL = `${req.protocol}://${req.get('host')}`
const args = 'foo=bar'
const callbackURL = `${serverURL}/${endpoint}?${args}`
return {callbackURL}
}
}
}
// In controller 'auth':
#UseGuards(DynamicAuthGuard('facebook')) // or any passport strategy
#Get('facebook/redirect')
async facebookRedirect(#Req() req: Request, #Res() res: Response) {
// ...
}

Related

NestJS Fastify JWKS Validation

I am using the Fastify Adapter in my NestJS application and would like to add some logic to do JWKS validation, similar to the passport example on the Auth0 website.
// src/authz/jwt.strategy.ts
import { Injectable } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { passportJwtSecret } from 'jwks-rsa';
import * as dotenv from 'dotenv';
dotenv.config();
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
secretOrKeyProvider: passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `${process.env.AUTH0_ISSUER_URL}.well-known/jwks.json`,
}),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
audience: process.env.AUTH0_AUDIENCE,
issuer: `${process.env.AUTH0_ISSUER_URL}`,
algorithms: ['RS256'],
});
}
validate(payload: unknown): unknown {
return payload;
}
}
It is my understanding that Passport only works with Express and will not work with Fastify. Does anyone know how to do something like this with Fastify and NestJS ?
I didn't manage to find a library like passport to do the JWKS validation with fastify. I decided to write my own validation using the jsonwebtoken and the #types/jsonwebtoken libraries.
Below is a sample of my solution for anybody else that is interested :)
File structure is as follows:
src
|__ auth
|__jwks
|_ jwks.client.ts
|_ jwks.service.ts
|_ jwt-auth.guard.ts
|_ jwt-auth.module.ts
|__ caching
|_ redis-cache.module.ts
|__ models
|__ json-web-key.model.ts
|__ jwks-response.model.ts
|__ my.controller.ts
|__ app.module.ts
models for the jwks response
// src/models/jwks-response.model.ts
import { JsonWebKey } from "src/models/json-web-key.model";
export class JwksResponse {
keys: Array<JsonWebKey>
}
// src/models/json-web-key.model.ts
export class JsonWebKey {
kty: string;
kid: string;
use: string;
x5t: string;
x5c: Array<string>;
n?: string;
e?: string;
x?: string;
y?: string;
crv?: string;
}
client to call the jwks endpoint and process the response
//src/auth/jwks/jwks.client.ts
import { HttpException, Injectable, Logger } from "#nestjs/common";
import { ConfigService} from "#nestjs/config";
import { HttpService } from "#nestjs/axios";
import { map, lastValueFrom } from "rxjs";
import { JwksResponse } from "src/models/jwks-response.model";
import { JsonWebKey } from "src/models/json-web-key.model";
#Injectable()
export class JwksClient {
private readonly logger: Logger = new Logger(JwksClient.name);
private readonly JWKS_URL: string = this.configService.get<string>('services.jwks.url');
private readonly TIMEOUT: number = parseInt(this.configService.get<string>('services.timeout'));
constructor(private configService: ConfigService, private httpService: HttpService){}
async getJsonWebKeySet(): Promise<Array<JsonWebKey>> {
this.logger.log(`Attempting to retrieve json web keys from Jwks endpoint`);
const config = {
timeout: this.TIMEOUT,
};
let response: JwksResponse = null;
try {
response = await lastValueFrom(this.httpService.get(this.JWKS_URL, config)
.pipe(
map((response) => {
return response.data;
},
),
),
);
} catch(e) {
this.logger.error(`An error occurred invoking Jwks endpoint to retrieve public keys`);
this.logger.error(e.stack);
throw new HttpException(e.message, e.response.status);
}
if (!response || !response.keys || response.keys.length == 0) {
this.logger.error('No json web keys were returned from Jwks endpoint')
return [];
}
return response.keys;
}
}
service containing logic to call jwks endpoint and verify the jwt token with the public key.
The JWT token will consist of a header, payload and a signature.
The header should also have a kid field that will match the kid of one of the json web keys, so that you know which one to verify your token with.
The x5c array contains a certificate chain and the first element of this array will always contain the certificate that you use to get the public key from to verify the token.
Note: I had to wrap the certificate in with \n-----BEGIN CERTIFICATE-----\n${key.x5c[0]}\n-----END CERTIFICATE----- to be able to create the public key but you may not have to do this in your implementation.
You will also need to add logic to verify the claims for your JWT.
I have also cached a valid JWT for a period of time to ensure that the verification is not required each time as this would have performance implications, the key for this cache uses the auth token to ensure that it is unique.
import { HttpException, HttpStatus, Injectable, CACHE_MANAGER, Logger, Inject } from "#nestjs/common";
import { ConfigService} from "#nestjs/config";
import { IncomingHttpHeaders } from "http";
import { JwksClient } from "src/auth/jwks/jwks.client";
import { JsonWebKey } from "src/models/json-web-key.model";
import { JwtPayload } from 'jsonwebtoken';
import * as jwt from 'jsonwebtoken';
import * as crypto from "crypto";
import { Cache } from 'cache-manager';
#Injectable()
export class JwksService {
private readonly logger: Logger = new Logger(JwksService.name);
private readonly CACHE_KEY: string = this.configService.get<string>('caches.jwks.key');
private readonly CACHE_TTL: number = parseInt(this.configService.get<string>('caches.jwks.ttl'));
constructor(private configService: ConfigService, private readonly jwksClient: JwksClient, #Inject(CACHE_MANAGER) private cacheManager: Cache){}
async verify(request: any): Promise<boolean> {
let token: string = this.getAuthorizationTokenFromHeader(request.headers);
const jwksKey = `${this.CACHE_KEY}:${token}`
const cachedVerificationResult: boolean = await this.getCachedVerificationResult(jwksKey);
if (cachedVerificationResult) {
this.logger.debug("Found cached verification result");
return cachedVerificationResult;
}
if (!this.hasTokenWithValidClaims(token)) {
this.logger.error("Token with invalid claims was provided")
return false;
}
// Get all web keys from JWKS endpoint
let jsonWebKeys: Array<JsonWebKey> = await this.jwksClient.getJsonWebKeySet();
// Find the public key with matching kid
let publicKey: string | Buffer = this.findPublicKey(token, jsonWebKeys);
if (!publicKey) {
this.logger.error("No public key was found for the bearer token provided")
return false;
}
try {
jwt.verify(token, publicKey, { algorithms: ['Put algorithm here e.g. HS256, ES256 etc'] });
} catch(e) {
this.logger.error("An error occurred verifying the bearer token with the associated public key");
this.logger.error(e.stack);
throw new HttpException(e.message, HttpStatus.FORBIDDEN);
}
// Cache Jwks validation result
this.cacheManager.set(jwksKey, true, { ttl: this.CACHE_TTL });
this.logger.debug("Successfully verified bearer token with the associated public key")
return true;
}
private hasTokenWithValidClaims(token: string) {
var { header, payload, signature } = jwt.decode(token, { complete: true });
// TODO: Add validation for claims
return true;
}
private findPublicKey(token: string, jsonWebKeys: Array<JsonWebKey>): string | Buffer {
var { header } = jwt.decode(token, { complete: true });
let key = null;
for (var jsonWebKey of jsonWebKeys) {
if (jsonWebKey.kid === header.kid) {
this.logger.debug(`Found json web key for kid ${header.kid}`);
key = jsonWebKey;
break;
}
}
if (!key) {
return null;
}
// Exctact x509 certificate from the certificate chain
const x509Certificate = `\n-----BEGIN CERTIFICATE-----\n${key.x5c[0]}\n-----END CERTIFICATE-----`;
// Create the public key from the x509 certificate
return crypto.createPublicKey(x509Certificate).export({type:'spki', format:'pem'})
}
private getAuthorizationTokenFromHeader(headers: IncomingHttpHeaders): string {
if(!headers || !headers.authorization) {
throw new HttpException("Authorization header is missing", HttpStatus.BAD_REQUEST);
}
let token: string = headers.authorization;
if (token.startsWith("Bearer ")) {
token = headers.authorization.split(" ")[1].trim();
}
return token;
}
private async getCachedVerificationResult(jwksKey: string): Promise<boolean> {
const response: boolean = await this.cacheManager.get(jwksKey);
if(response && response === true) {
return response;
}
return null;
}
}
guard to verify the JWT
// src/auth/jwks/jwt-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, Logger } from '#nestjs/common';
import { JwksService } from 'src/auth/jwks/jwks.service';
#Injectable()
export class JwtAuthGuard implements CanActivate {
private readonly logger: Logger = new Logger(JwtAuthGuard.name);
constructor(private jwksService: JwksService){}
async canActivate(
context: ExecutionContext,
): Promise<boolean> {
const request = context.switchToHttp().getRequest();
return await this.jwksService.verify(request);
}
}
module containing config for jwks
// src/auth/jwks/jwt-auth.model.ts
import { Module } from '#nestjs/common';
import { ConfigModule } from '#nestjs/config';
import { HttpModule } from '#nestjs/axios';
import configuration from '../../../config/configuration';
import { JwksClient } from 'src/auth/jwks/jwks.client';
import { JwksService } from 'src/auth/jwks/jwks.service';
#Module({
imports: [
ConfigModule.forRoot({ load: [configuration] }),
HttpModule
],
providers: [
JwksClient,
JwksService,
],
exports: [JwksService, JwksClient],
})
export class JwtAuthModule {}
redis caching module containing config for redis cache
// src/caching/redis-cache.module.ts
import { CacheModule, Module } from '#nestjs/common';
import { ConfigModule, ConfigService } from '#nestjs/config';
import configuration from '../../config/configuration';
import { RedisClientOptions } from 'redis';
import * as redisStore from 'cache-manager-redis-store';
#Module({
imports: [
ConfigModule.forRoot({ load: [configuration] }),
CacheModule.registerAsync<RedisClientOptions>({
isGlobal: true,
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
store: redisStore,
host: process.env.REDIS_URL,
port: configService.get<number>('redis.port'),
password: configService.get<string>('redis.password'),
tls: configService.get<boolean>('redis.tls')
}),
inject: [ConfigService],
})
],
controllers: [],
providers: []
})
export class RedisCacheModule {}
controller that uses the JwtAuthGuard
// src/my.controller.ts
import { Controller, Get, Param, Logger } from '#nestjs/common';
#Controller()
#UseGuards(JwtAuthGuard)
export class MyController {
private readonly logger: Logger = new Logger(MyController.name);
#Get('/:id')
async getCustomerDetails(#Headers() headers, #Param('id') id: string): Promise<Customer> {
this.logger.log(`Accepted incoming request with id: ${id}`);
// Do some processing ....
return customer;
}
}
module containing configuration for whole app
// src/app.module.ts
import { Module } from '#nestjs/common';
import { ConfigModule } from '#nestjs/config';
import { HttpModule } from '#nestjs/axios';
import configuration from '../config/configuration';
import { JwtAuthModule } from 'src/auth/jwks/jwt-auth.module';
import { RedisCacheModule } from 'src/caching/redis-cache.module';
#Module({
imports: [
ConfigModule.forRoot({ load: [configuration] }),
HttpModule,
JwtAuthModule,
RedisCacheModule
],
controllers: [MyController],
providers: []
})
export class AppModule {}

Cannot read property forEach of undefined

The title of this question is just the error I am currently receiving, but what I really need help with is understanding observables and API calls. For whatever reason, I just haven't been able to get a good grasp of this concept, and I am hoping that someone might have an explanation that will finally click.
I am trying to create a new Angular service that retrieves JSON from an API. I then need to map the response to a model. Due to weird naming conventions, job descriptions and job requirements are used interchangeably here. Here is my service class.
import { CommunicationService } from './communication.service';
import { AiDescription } from '../models/ai-description.model';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
#Injectable()
export class AiDescriptionService {
requirements: Observable<AiDescription[]>;
private aiDescriptionUrl: string = '/api/core/company/jobdescriptions';
private dataStore: {
requirements: AiDescription[]
};
private _requirements: BehaviorSubject<AiDescription[]>;
private emptyRequestParams = {
"company_id": "",
"carotene_id": "",
"carotene_version": "",
"city": "",
"state": "",
"country": ""
};
readonly caroteneVersion: string = "caroteneV3";
constructor(
private communicationService: CommunicationService
) {
this.dataStore = { requirements: [] };
this._requirements = new BehaviorSubject<AiDescription[]>([]);
this.requirements = this._requirements.asObservable();
}
LoadRequirements(params: Object) {
this.communicationService.postData(this.aiDescriptionUrl, params)
.subscribe(res => {
let jobDescriptions = [];
jobDescriptions = res.jobdescriptions;
jobDescriptions.forEach((desc: { id: string; description: string; }) => {
let aiDescription = new AiDescription();
aiDescription.id = desc.id;
aiDescription.description = desc.description;
});
this.dataStore.requirements = res;
this._requirements.next(Object.assign({}, this.dataStore).requirements);
});
}
CreateRequest(
companyID : string,
caroteneID : string,
city: string,
state: string,
country: string
): Object {
let newRequestParams = this.emptyRequestParams;
newRequestParams.company_id = companyID;
newRequestParams.carotene_id = caroteneID;
newRequestParams.carotene_version = this.caroteneVersion;
newRequestParams.city = city;
newRequestParams.state = state;
newRequestParams.country = country;
this.LoadRequirements(newRequestParams);
return this.dataStore;
}
}
The postData() function being called by this.communicationService is here:
postData(url: string, jobInformation: any): Observable<any> {
const start = new Date();
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
const body = JSON.stringify(jobInformation);
const options = { headers };
return this.http.post(url, body, options)
.catch(err => Observable.throw(err))
.do(() => {
this.analyticsLoggingService.TrackTiming('JobPostingService', 'PostSuccess', new Date().getTime() - start.getTime());
}, () => {
this.analyticsLoggingService.TrackError('JobPostingService', 'PostFailure');
});
}
I didn't write the postData function, and I would not be able to modify it. When running a unit test, I am getting this error: "TypeError: Cannot read property 'forEach' of undefined".
But more than simply fixing the error, I am really trying to get a better understanding of using Observables, which is something I haven't been able to get a good understanding of from other sources.
In your example, I recommend replacing any and Object with explicitly defined models.
Here's an example for Angular 8 for Subscription, Promise, and Observable API calls. You can get more info here: https://angular.io/tutorial/toh-pt6.
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '#angular/common/http';
import { Observable } from 'rxjs';
import { User } from './user.model';
#Injectable({ providedIn: 'root' })
export class UserService {
users: User[];
authHeaders = new HttpHeaders()
.set('Content-Type', 'application/json');
constructor(
private readonly http: HttpClient
) { }
getUsers() {
this.http.get(`https://myApi/users`, { headers: this.authHeaders })
.subscribe(
(data: User[]) => {
this.users = data;
}, (error: HttpErrorResponse) => { /* handle error */ });
}
async getUserPromise(userID: number): Promise<User> {
const url = `https://myApi/users/${userID}`;
return this.http.get<User>(url, { headers: this.authHeaders })
.toPromise();
}
getUserObservable(userID: number): Observable<User> {
const url = `https://myApi/users/${userID}`;
return this.http.get<User>(url, { headers: this.authHeaders });
}
}
I like to keep my class models in separate files. This example would have user.model.ts with content like:
export class User {
constructor(
public id: number,
public username: string,
public displayName: string,
public email: string
) { }
}
I've not included authentication headers or error handling for brevity; however, you might want to add those as needed.

Ionic 3 + HttpClientModule and token from storage

I have build an interceptor for making HTTP requests to a PHP backend.
This backend gives an JWT token to the app and I save this in the Ionic Storage.
But I want to get that token from Storage and add it as an header to the HTTP request.
Below is my interceptor with and hardcoded token.
This works and I get a response from the backend.
See update # bottom of this post
http-interceptor.ts
import { HttpInterceptor, HttpRequest } from '#angular/common/http/';
import {HttpEvent, HttpHandler} from '#angular/common/http';
import { AuthProvider } from "../providers/auth/auth";
import {Injectable} from "#angular/core";
import {Observable} from "rxjs/Observable";
import {Storage} from "#ionic/storage";
#Injectable()
export class TokenInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const changedReq = req.clone({headers: req.headers.set('Authorization', 'Bearer MY TOKEN')});
return next.handle(changedReq);
}
}
But how do I get the token from storage into the header.
I searched alot and most of the tutorials / examples are from the older HTTP module. If someone has an idea or has a up2date example ?
UPDATE
Oke below code send the token
intercept(req: HttpRequest<any>, next: HttpHandler) : Observable<HttpEvent<any>>{
return fromPromise(this.Auth.getToken())
.switchMap(token => {
const changedReq = req.clone({headers: req.headers.set('Authorization', 'Bearer ' + token )});
return next.handle(changedReq);
});
}
With 1 exception, namely the first time I access that page :)
You can save JWT token in, for example, localStorage
localStorage.setItem('myToken', res.token);
and then access it with
localStorage.getItem('myToken');
In your case something like this:
import { HttpInterceptor, HttpRequest } from '#angular/common/http/';
import {HttpEvent, HttpHandler} from '#angular/common/http';
import { AuthProvider } from "../providers/auth/auth";
import {Injectable} from "#angular/core";
import {Observable} from "rxjs/Observable";
import {Storage} from "#ionic/storage";
#Injectable()
export class TokenInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const changedReq = req.clone({headers: req.headers.set('Authorization', localStorage.getItem('myToken'))});
return next.handle(changedReq);
}
}
If you want to use Ionic Storage
import { HttpInterceptor, HttpRequest } from '#angular/common/http/';
import {HttpEvent, HttpHandler} from '#angular/common/http';
import { AuthProvider } from "../providers/auth/auth";
import {Injectable} from "#angular/core";
import {Observable} from "rxjs/Observable";
import {Storage} from "#ionic/storage";
#Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(public _storage: Storage) {
_storage.get('myToken').then((val) => {
console.log('Your age is', val);
});
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const changedReq = req.clone({headers: req.headers.set('Authorization', this.val)});
return next.handle(changedReq);
}
}
Caching the token in the interceptor is a bad idea because if the token changes the interceptor will not be aware of those changes.
// Don't do this.
token: string;
constructor(private storage: Storage) {
this.storage.get('token').then((res) => {
this.token = res;
})
}
If you want to use Ionic Storage and the interceptor together you can do so by using Observable.flatMap like so...
app.module.ts
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true},
SecurityStorageService
]
AuthInterceptor.ts
#Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(
private securityStorageService: SecurityStorageService
) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// This method gets a little tricky because the security token is behind a
// promise (which we convert to an observable). So we need to concat the
// observables.
// 1. Get the token then use .map to return a request with the token populated in the header.
// 2. Use .flatMap to concat the tokenObservable and next (httpHandler)
// 3. .do will execute when the request returns
const tokenObservable = this.securityStorageService.getSecurityTokenAsObservable().map(token => {
return request = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
});
return tokenObservable.flatMap((req) => {
return next.handle(req).do((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
// do stuff to the response here
}
}, (err: any) => {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
// not authorized error .. do something
}
}
});
})
}
security-storage-service.ts
You technically don't need this service, but you shouldn't have Ionic Storage logic in your interceptor.
#Injectable()
export class SecurityStorageService {
constructor(private storage: Storage) {
}
getSecurityToken() {
return this.storage.get(StorageKeys.SecurityToken)
.then(
data => { return data },
error => console.error(error)
);
}
getSecurityTokenAsObservable() {
return Observable.fromPromise(this.getSecurityToken());
}
}
storage-keys.ts
export class StorageKeys {
public static readonly SecurityToken: string = "SecurityToken";
}
For anyone who comes across this like me and is using rxjs >=5.5.0 then you can just do:
auth-interceptor.ts
#Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return from(this.authService.getToken()).pipe(mergeMap((token) => {
const changedReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(changedReq);
}));
}
auth-service.ts
public async getToken() {
return await this.storage.get('ACCESS_TOKEN');
}

Angular 2 data service

I'm building an observable data service based on the following article: https://coryrylan.com/blog/angular-2-observable-data-services
In the article he used an array as an example, here I will use the user object since I'm developing the user service.
Here's what I got:
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Events, SqlStorage, Storage } from 'ionic-angular';
import { Subject } from 'rxjs/Subject';
export interface DataStore {
user: Object
}
#Injectable()
export class UserService {
private baseUrl: string;
private storage: Storage;
private _user$: Subject<Object>;
private dataStore: DataStore;
constructor(
private http: Http
) {
this.baseUrl = 'http://localhost:3000';
this.storage = new Storage(SqlStorage);
this._user$ = <Subject<Object>>new Subject();
this.dataStore = {
user: { name: '' }
};
}
set user$(user: Object) {
this.storage.set('user', JSON.stringify(user));
this.dataStore.user = user;
this._user$.next(this.dataStore.user);
}
get user$() {
return this._user$.asObservable();
}
loadUser() {
return this.storage.get('user').then(
((user: string): Object => {
this.dataStore.user = JSON.parse(user);
this._user$.next(this.dataStore.user);
return this.dataStore.user;
})
);
}
login(accessToken: string) {
return this.http
.post('http://localhost:3000/login', { access_token: accessToken })
.retry(2)
.map((res: Response): any => res.json());
}
logout(): void {
this.storage.remove('user');
}
}
To authenticate I call the login() function and set the user data if everything ok.
this.userService.login(this.data.accessToken)
.subscribe(
(user: Object) => {
this.userService.user$ = user;
this.nav.setRoot(EventListComponent);
},
(error: Object) => console.log(error)
);
I feel it is better set the user data inside the service. I could do the following:
login(accessToken: string) {
return this.http
.post('http://localhost:3000/login', {
access_token: accessToken
})
.retry(2)
.map((res: Response): any => res.json())
.subscribe(
(user: Object) => {
this.userService.user$ = user;
this.nav.setRoot(EventListComponent);
},
(error: Object) => console.log(error)
);
}
But I won't be able to subscribe to the login() function in the component since it's already subscribed. How could I redirect the user if everything ok or show an alert if anything goes wrong in the component but setting the user inside the service?
In the main component I load the user data and set the rootPage:
this.userService.loadUser().then(
(user: Object) => this.rootPage = EventListComponent,
(error: Object) => this.rootPage = LoginComponent
);
I thought that calling the loadUser() function at this time I would not have to call it again, but I have to call it in all components that I need the user data:
this.user = this.userService.user$;
this.userService.loadUser();
I don't think the service is the way it should, what could I improve? Is there any better way to achieve what I want? Any example or idea?

AngularJS 2 Typescript interface

I have a service for handling users operations and an interface for the user object.
user.service.ts
import {Injectable} from 'angular2/core';
export interface User {
name: string;
email?: string;
picture?: string;
}
#Injectable()
export class UserService {
me: User;
constructor() {
}
setUser(user: User) {
this.me = user;
}
}
In my login component I try to set the user with the profile returned from the login service but I get this error:
Property 'firstName' does not exist on type '{}'.
login.component.ts
import {Component} from 'angular2/core';
import {User, UserService} from './services/user.service';
import {LinkedinService} from './services/linkedin.service';
declare const IN: any;
console.log('`Login` component loaded asynchronously');
#Component({
selector: 'Login',
providers: [
UserService,
LinkedinService
],
template: require('./login.html')
})
export class LoginComponent {
me: User;
constructor(public linkedinService: LinkedinService, public userService: UserService) {
this.me = userService.me;
}
ngOnInit() {
console.log('hello `Login` component');
}
login() {
this.linkedinService.login()
.then(() => this.linkedinService.getMe()
.then(profile => this.userService.setUser({ name: profile.firstName })));
}
}
linkedin.service.ts
import {Injectable} from 'angular2/core';
declare const IN: any;
#Injectable()
export class LinkedinService {
constructor() {
IN.init({
api_key: 'xxxxxxxxxxx',
authorize: true
});
}
login() {
return new Promise((resolve, reject) => {
IN.User.authorize(() => resolve());
});
}
getMe() {
return new Promise((resolve, reject) => {
IN.API.Profile('me').result((profile) => resolve(profile.values[0]));
});
}
}
I'm trying to import the User interface from UserService and use inside the LoginComponent but I don't know what I'm doing wrong. Any idea? I am not sure if I have to use the User interface inside the LoginComponent, is that right?
Narrow in on the code :
.then(() => this.linkedinService.getMe())
.then(profile => this.userService.setUser({ name: profile.firstName })));
The type of profile is driven by the response of this.linkedinService.getMe(). Seems like it is something like Promise<{}>. It does not have the member firstName. Hence the error:
Property 'firstName' does not exist on type '{}'.
Fix
Check to the code / signatures of linkedinService. This has nothing to do with the user.service.ts file that the question contains 🌹
Update
Focus in on the code:
getMe() {
return new Promise((resolve, reject) => {
IN.API.Profile('me').result((profile) => resolve(profile.values[0]));
});
}
The value returned is driven by what is being passed to resolve. So make sure profile.values[0] has the right type. Alternatively provide the hint to the compiler:
getMe() {
return new Promise<{firstName:string}>((resolve, reject) => {
IN.API.Profile('me').result((profile) => resolve(profile.values[0]));
});
}