Skip to content

Commit

Permalink
Implemented Facebook Login without the SDK. Here is the login flow:
Browse files Browse the repository at this point in the history
You click the Facebook Login button and a request is send to the server to get the redirect_uri to the Facebook login page (the user is redirected when the redirect_uri is returned). Upon entering credentials and giving consent to the application, the user gets redirected back to the app with queryParams in the url containing a code which is then used to obtain an access_token from Facebook. This access_token is returned to the client and the client sends a request with it to the server to invoke the passport-facebook-token strategy and handle the user.
  • Loading branch information
bojidaryovchev committed Apr 21, 2018
1 parent 979b4ba commit efddd5c
Show file tree
Hide file tree
Showing 12 changed files with 159 additions and 4 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"@nestjs/websockets": "4.6.6",
"@nguniversal/express-engine": "5.0.0-beta.6",
"@nguniversal/module-map-ngfactory-loader": "5.0.0-beta.6",
"@types/request": "2.47.0",
"body-parser": "1.18.2",
"bootstrap": "3.3.7",
"cookie-parser": "1.4.3",
Expand All @@ -98,10 +99,12 @@
"jsonwebtoken": "8.1.1",
"mongoose": "4.13.9",
"passport": "0.4.0",
"passport-facebook-token": "^3.3.0",
"passport-jwt": "3.0.1",
"passport-local": "1.0.0",
"redis": "2.8.0",
"reflect-metadata": "0.1.10",
"request": "2.85.0",
"rxjs": "5.5.6",
"zone.js": "0.8.20"
}
Expand Down
1 change: 1 addition & 0 deletions src/client/app/components/recipes/recipes.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<button mat-button (click)="signUp()">Sign Up</button>
<button mat-button (click)="signIn()">Sign In</button>
<button mat-button (click)="getProtected()">Get Protected</button>
<button mat-button (click)="facebookLogin()">Facebook Login</button>
</div>
<div class="col-md-6">
<router-outlet></router-outlet>
Expand Down
24 changes: 23 additions & 1 deletion src/client/app/components/recipes/recipes.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RecipeItem } from './recipes-list/recipe-item/recipe-item.component';
import { RecipesService } from '../../services/recipes.service';
import { AuthService } from '../../services/auth.service';
import { FormControl, Validators } from '@angular/forms';
import { ActivatedRoute, Params, Router } from '@angular/router';

@Component({
selector: 'app-recipes',
Expand All @@ -26,10 +27,24 @@ export class RecipesComponent implements OnInit {

constructor(
private readonly recipesService: RecipesService,
private readonly authService: AuthService
private readonly authService: AuthService,
private readonly route: ActivatedRoute,
private readonly router: Router
) { }

ngOnInit() {
this.route.queryParams.subscribe((params: Params) => {
if (Object.keys(params).length) {
this.authService.requestFacebookAccessToken(params.code)
.subscribe((params: Params) => {
this.authService.facebookSignIn(params.access_token)
.subscribe((params: Params) => {
this.router.navigate(['/']);
});
});
}
});

this.recipesService.getRecipes()
.then((recipes: RecipeItem[]) => this.recipes = recipes);
}
Expand Down Expand Up @@ -59,4 +74,11 @@ export class RecipesComponent implements OnInit {
console.log(res);
});
}

facebookLogin() {
this.authService.requestFacebookRedirectUri()
.subscribe((response: {redirect_uri: string}) => {
window.location.replace(response.redirect_uri);
});
}
}
12 changes: 12 additions & 0 deletions src/client/app/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ export class AuthService {
});
}

requestFacebookRedirectUri(): Observable<any> {
return this.httpClient.get('api/auth/facebook/uri');
}

requestFacebookAccessToken(code: string): Observable<any> {
return this.httpClient.post('api/auth/facebook/token', { code });
}

facebookSignIn(access_token: string) {
return this.httpClient.post('api/auth/facebook/signin', { access_token });
}

getProtected(): Observable<any> {
return this.httpClient.get(`api/auth/authorized`);
}
Expand Down
15 changes: 15 additions & 0 deletions src/server/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ export class AuthController {
return await this.authService.createToken(req.user);
}

@Get('facebook/uri')
async requestFacebookRedirectUrl(): Promise<{redirect_uri: string}> {
return await this.authService.requestFacebookRedirectUri();
}

@Post('facebook/token')
async requestFacebookAccessToken(@Req() req: Request): Promise<IToken> {
return await this.authService.requestFacebookAccessToken(req.body.code);
}

@Post('facebook/signin')
async facebookSignIn(@Req() req: Request): Promise<any> {
return await this.authService.createToken(req.user);
}

@Get('authorized')
public async authorized() {
console.log('Authorized route...');
Expand Down
11 changes: 10 additions & 1 deletion src/server/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@ import {
import { authenticate } from 'passport';

import { UserModule } from '../user/user.module';
import { authProviders } from './auth.providers';
import { AuthService } from './auth.service';
import { LocalStrategy } from './passport/local.strategy';
import { JwtStrategy } from './passport/jwt.strategy';
import { FacebookStrategy } from './passport/facebook.strategy';
import { AuthController } from './auth.controller';
import { bodyValidatorMiddleware } from './middlewares/body-validator.middleware';


@Module({
imports: [UserModule],
components: [
...authProviders,
AuthService,
LocalStrategy,
JwtStrategy
JwtStrategy,
FacebookStrategy
],
controllers: [AuthController],
exports: [AuthService]
Expand All @@ -39,6 +44,10 @@ export class AuthModule implements NestModule {
])
.forRoutes({ path: 'api/auth/signin', method: RequestMethod.POST });

consumer
.apply(authenticate('facebook', { session: false }))
.forRoutes({ path: 'api/auth/facebook/signin', method: RequestMethod.POST });

consumer
.apply(authenticate('jwt', { session: false }))
.forRoutes({ path: 'api/auth/authorized', method: RequestMethod.ALL });
Expand Down
9 changes: 9 additions & 0 deletions src/server/modules/auth/auth.providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FACEBOOK_CONFIG_TOKEN } from '../../server.constants';
import { facebookConfig } from './config/facebook-config';

export const authProviders = [
{
provide: FACEBOOK_CONFIG_TOKEN,
useValue: facebookConfig
}
];
44 changes: 42 additions & 2 deletions src/server/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { Component, Inject } from '@nestjs/common';
import { Model } from 'mongoose';
import { sign } from 'jsonwebtoken';
import { get, Response } from 'request';

import { SERVER_CONFIG, USER_MODEL_TOKEN } from '../../server.constants';
import { SERVER_CONFIG, USER_MODEL_TOKEN, FACEBOOK_CONFIG_TOKEN } from '../../server.constants';
import { IUser } from '../user/interfaces/user.interface';
import { IToken } from './interfaces/token.interface';
import { IFacebookConfig } from './interfaces/facebook-config.interface';

@Component()
export class AuthService {
constructor(
@Inject(USER_MODEL_TOKEN) private readonly userModel: Model<IUser>
@Inject(USER_MODEL_TOKEN) private readonly userModel: Model<IUser>,
@Inject(FACEBOOK_CONFIG_TOKEN) private readonly fbConfig: IFacebookConfig
) {}

async createToken(user: IUser): Promise<IToken> {
Expand All @@ -32,4 +35,41 @@ export class AuthService {

return false;
}

async requestFacebookRedirectUri(): Promise<{redirect_uri: string}> {
const queryParams: string[] = [
`client_id=${this.fbConfig.client_id}`,
`redirect_uri=${this.fbConfig.oauth_redirect_uri}`,
`state=${this.fbConfig.state}`
];
const redirect_uri: string = `${this.fbConfig.login_dialog_uri}?${queryParams.join('&')}`;

return {
redirect_uri
};
}

async requestFacebookAccessToken(code: string): Promise<any> {
const queryParams: string[] = [
`client_id=${this.fbConfig.client_id}`,
`redirect_uri=${this.fbConfig.oauth_redirect_uri}`,
`client_secret=${this.fbConfig.client_secret}`,
`code=${code}`
];
const uri: string = `${this.fbConfig.access_token_uri}?${queryParams.join('&')}`;

return new Promise((resolve: Function, reject: Function) => {
get(uri, (error: Error, response: Response, body: any) => {
if (error) {
return reject(error);
}

if (body.error) {
return reject(body.error);
}

resolve(body);
});
});
}
}
10 changes: 10 additions & 0 deletions src/server/modules/auth/config/facebook-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IFacebookConfig } from '../interfaces/facebook-config.interface';

export const facebookConfig: IFacebookConfig = {
login_dialog_uri: 'https://www.facebook.com/v2.12/dialog/oauth',
access_token_uri: 'https://graph.facebook.com/v2.12/oauth/access_token',
client_id: '545785722443649',
client_secret: 'a7b3b484ed5b0217b89824541eca1f4e',
oauth_redirect_uri: 'http://localhost:4200/recipes',
state: '{fbstate}'
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface IFacebookConfig {
login_dialog_uri: string;
access_token_uri: string;
client_id: string;
client_secret: string;
oauth_redirect_uri: string;
state: string;
}
25 changes: 25 additions & 0 deletions src/server/modules/auth/passport/facebook.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Component, Inject } from '@nestjs/common';
import { use } from 'passport';

import { FACEBOOK_CONFIG_TOKEN } from '../../../server.constants';
import { IFacebookConfig } from '../interfaces/facebook-config.interface';

const FacebookTokenStrategy = require('passport-facebook-token');

@Component()
export class FacebookStrategy {
constructor(
@Inject(FACEBOOK_CONFIG_TOKEN) private readonly fbConfig: IFacebookConfig
) {
this.init();
}

private init(): void {
use('facebook', new FacebookTokenStrategy({
clientID: this.fbConfig.client_id,
clientSecret: this.fbConfig.client_secret
}, function(accessToken: string, refreshToken: string, profile: any, done: Function) {
console.log(profile);
}))
}
}
1 change: 1 addition & 0 deletions src/server/server.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const SERVER_CONFIG: IEnvironmentConfig = Config[env];

export const DB_CONNECTION_TOKEN: string = 'DbConnectionToken';
export const USER_MODEL_TOKEN: string = 'UserModelToken';
export const FACEBOOK_CONFIG_TOKEN: string = 'FacebookConfigToken';

export const MESSAGES = {
UNAUTHORIZED_EMAIL_IN_USE: 'The email already exists',
Expand Down

0 comments on commit efddd5c

Please sign in to comment.