Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Improve authentication (avoid re-login on refresh) #12

Merged
merged 3 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Component, inject, OnInit} from '@angular/core';
import {AuthService} from "./general/service/auth.service";
import {ActivatedRoute, Router} from "@angular/router";
import {AuthService} from './general/service/auth.service';
import {ActivatedRoute, Router} from '@angular/router';

@Component({
selector: 'app-root',
Expand All @@ -14,14 +14,16 @@ export class AppComponent implements OnInit {
private auth: AuthService,
private router: Router
) {
console.log("AppComponent.constructor")
console.log('AppComponent.constructor');
}

ngOnInit() {
if (this.auth.userSignedIn) {
this.router.navigate(["/manager"])
} else {
this.router.navigate(["/login"])
}
async ngOnInit() {
// console.log("AppComponent.ngOnInit");
// if (!await this.auth.checkIfUserAuthenticated()) {
// this.router.navigate(['/login']);
// }
// console.log("AppComponent.ngOnInit end");
}


}
6 changes: 5 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { LoginHomeComponent } from './login-home/login-home.component';
import {MatGridListModule} from "@angular/material/grid-list";
import {MatCardModule} from "@angular/material/card";
import {MatButtonModule} from "@angular/material/button";
import {DynamicScriptLoaderService} from './general/service/dynamic-script-loader.service';
import {MatProgressBarModule} from '@angular/material/progress-bar';


@NgModule({
Expand All @@ -34,7 +36,8 @@ import {MatButtonModule} from "@angular/material/button";
HttpClientModule,
MatGridListModule,
MatCardModule,
MatButtonModule
MatButtonModule,
MatProgressBarModule
],
declarations: [
AppComponent,
Expand All @@ -43,6 +46,7 @@ import {MatButtonModule} from "@angular/material/button";
providers: [
DiagramService,
AuthService,
DynamicScriptLoaderService,
LoggedInGuard,
UserPreferenceService,
GoogleDriveService,
Expand Down
11 changes: 8 additions & 3 deletions src/app/data-access/service/google-drive.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Injectable} from '@angular/core';
import {AuthService} from '../../general/service/auth.service';
import {HttpClient} from '@angular/common/http';
import {DiagramMetadata} from '../model/diagram-item.model';
import {environment} from '../../../environments/environment';

declare let gapi: any;

Expand Down Expand Up @@ -49,7 +50,11 @@ export class GoogleDriveService {

public async init(): Promise<void> {
if (!this.initialized) {
// console.log('GoogleDriveService.init');
console.log('GoogleDriveService.init token', this.auth.accessToken);
gapi.client.setToken({access_token: this.auth.accessToken});
gapi.client.setApiKey(environment.gapi.api_key);
gapi.client.load('drive', 'v3');

await gapi.client.load('drive', 'v3');
this.initialized = true;
}
Expand Down Expand Up @@ -99,7 +104,7 @@ export class GoogleDriveService {
const endpoint = 'https://www.googleapis.com/upload/drive/v3/files/' + id + '?uploadType=multipart&fields=id';
return this.http.patch(endpoint, form, {
headers: {
Authorization: this.auth.getAuthorizationHeader()
Authorization: await this.auth.getAuthorizationHeader()
}
}).toPromise();
} else {
Expand All @@ -108,7 +113,7 @@ export class GoogleDriveService {
const endpoint = 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id';
return this.http.post(endpoint, form, {
headers: {
Authorization: this.auth.getAuthorizationHeader()
Authorization: await this.auth.getAuthorizationHeader()
}
}).toPromise();
}
Expand Down
192 changes: 106 additions & 86 deletions src/app/general/service/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,137 +1,157 @@
import {EventEmitter, Injectable} from '@angular/core';
import {environment} from '../../../environments/environment';
import TokenClient = google.accounts.oauth2.TokenClient;
import {DynamicScriptLoaderService} from './dynamic-script-loader.service';
import {GoogleDriveService} from '../../data-access/service/google-drive.service';

export type Profile = {
name: string,
email: string,
};

const AUTH_KEY = 'vect.AuthService.auth';
declare let gapi: any;
const GOOGLE_PROFILE_URL = `https://www.googleapis.com/oauth2/v3/userinfo`;


@Injectable()
export class AuthService {

public authenticatedEvent = new EventEmitter<Profile>();

tokenClient!: TokenClient;
accessToken?: string;

authInited = false;
gapiInited = false;
userSignedIn = false;
inited = false;
userAuthenticated = false;

private profile: any;
public name!: string;
public email!: string;
private authenticated!: EventEmitter<any>;
public profile?: Profile;

constructor() {
this.handleAuthResponse = this.handleAuthResponse.bind(this);
constructor(
protected dynamicScriptLoader: DynamicScriptLoaderService
) {
this.handleTokenResponse = this.handleTokenResponse.bind(this);
this.handleProfileResponse = this.handleProfileResponse.bind(this);
}


/**
* Callback after Google Identity Services are loaded.
*/
googleOAuthInit() {
public async checkIfUserAuthenticated(): Promise<boolean> {
console.log('AuthService.checkIfUserAuthenticated before', this.userAuthenticated);
if (!this.userAuthenticated) {
await this.init();
const storedAccessToken = localStorage.getItem(AUTH_KEY);
if (storedAccessToken) {
await this.requestProfile(storedAccessToken);
this.accessToken = storedAccessToken;
}
}
console.log('AuthService.checkIfUserAuthenticated after', this.userAuthenticated);
return this.userAuthenticated;
}


protected async init(): Promise<void> {
if (!this.inited) {
await this.initGoogleScripts();
console.log('AuthService.initialize google scripts loaded');
this.initTokenClient();
}
}

protected initGoogleScripts(): Promise<any> {
console.log('AuthService.initGoogleScripts');
const p1 = this.dynamicScriptLoader.loadScript('https://accounts.google.com/gsi/client');
const p2 = this.dynamicScriptLoader.loadScript('https://apis.google.com/js/api.js');
const p3 = this.dynamicScriptLoader.loadScript('https://apis.google.com/js/client:plusone.js');
return Promise.all([p1, p2, p3]);
}


protected initTokenClient(): void {
console.log('AuthService.initTokenClient');
this.tokenClient = google.accounts.oauth2.initTokenClient({
client_id: environment.gapi.client_id,
scope: environment.gapi.scope,
callback: this.handleAuthResponse
callback: this.handleTokenResponse
});
this.authInited = true;
console.log("GoogleUtils.gisLoaded inited")
this.inited = true;
}


async handleAuthResponse(res: any) {
private async handleTokenResponse(res: any): Promise<void> {
console.log('AuthService.handleTokenResponse', res);
if (res.error !== undefined) {
this.userSignedIn = false;
this.userAuthenticated = false;
throw (res);
}
console.log("GoogleUtils.handleAuthClick resp", res);
this.requestProfile(res.access_token)

if (res && res.access_token) {
gapi.client.setApiKey(environment.gapi.api_key);
gapi.client.load('drive', 'v3');
// await gapi.client.init({
// apiKey: environment.gapi.api_key,
// discoveryDocs: environment.gapi.discoveryDocs,
// });
this.gapiInited = true;
}
await this.requestProfile(res.access_token);

}
// wrong location?
// gapi.client.setApiKey(environment.gapi.api_key);
// gapi.client.load('drive', 'v3');

this.accessToken = res.access_token;
localStorage.setItem(AUTH_KEY, res.access_token);
}
}

requestProfile(accessToken: any) {
console.log("getUserProfileData", accessToken)
let promise = new Promise(function (resolve, reject) {
let request = new XMLHttpRequest();
const url = `https://www.googleapis.com/oauth2/v3/userinfo`;
request.addEventListener("loadend", function () {

const response = JSON.parse(this.responseText);
console.log("getUserProfileData loadend response", response)

if (this.status === 200) {
resolve(response);
} else {
// @ts-ignore
reject(this, response);
}
});
request.open("GET", url, true);
request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
request.send();
});

console.log("getUserProfileData then");
async requestProfile(accessToken: string): Promise<void> {
console.log('AuthService.requestProfile', accessToken);

promise.then(
this.handleProfileResponse, function (errorMessage) {
console.error(errorMessage);
});
const res = await fetch(GOOGLE_PROFILE_URL, {
method: 'get',
headers: new Headers({
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
})
});

if (res.ok) {
const profileResponse = await res.json();
await this.handleProfileResponse(profileResponse);
}
console.log('AuthService.requestProfile end');
}

handleProfileResponse(profileResponse: any) {
this.profile = profileResponse;
this.email = profileResponse.email;
this.name = profileResponse.name;
this.userSignedIn = true;
this.authenticated.emit(this.profile);
console.log("getUserProfileData response", profileResponse);

private async handleProfileResponse(profileResponse: any) {
console.log('AuthService.handleProfileResponse response', profileResponse);
this.profile = {
name: profileResponse.name,
email: profileResponse.email
};
this.userAuthenticated = true;
console.log('AuthService.handleProfileResponse authenticatedEvent.emit', this.profile);
this.authenticatedEvent.emit(this.profile);
}


/**
* Sign in the user upon button click.
*/
handleAuthClick() {
const token = gapi.client.getToken()
handleAuthClick(): void {
const token = gapi.client.getToken();
if (token) {
console.log("AuthService.handleAuthClick Skip")
console.log('AuthService.handleAuthClick Skip');
// Skip display of account chooser and consent dialog for an existing session.
this.tokenClient.requestAccessToken({prompt: '', login_hint: 'Super Hint!'});
this.tokenClient.requestAccessToken({prompt: '', state: AUTH_KEY});
} else {
console.log("AuthService.handleAuthClick Prompt the user to select")
console.log('AuthService.handleAuthClick Prompt the user to select');
// Prompt the user to select a Google Account and ask for consent to share their data
// when establishing a new session.
this.tokenClient.requestAccessToken({prompt: 'consent'});
this.tokenClient.requestAccessToken({prompt: 'consent', state: AUTH_KEY});
}
}


initialize(authenticated: EventEmitter<any>) {
this.authenticated = authenticated;
this.googleOAuthInit();
}

checkIfUserAuthenticated() {
console.log("AuthService.checkIfUserAuthenticated", this.userSignedIn)

return this.userSignedIn;
public async getAuthorizationHeader(): Promise<string> {
if (await this.checkIfUserAuthenticated()) {
return 'Bearer ' + this.accessToken;
} else {
throw new Error('User is not authenticated');
}
}

public getAuthorizationHeader(): string {
const token = gapi.auth.getToken();
// @ts-ignore
return token.token_type + ' ' + token.access_token;
public get allowToSignIn(): boolean {
return this.inited;
}

}
Expand Down
23 changes: 23 additions & 0 deletions src/app/general/service/dynamic-script-loader.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class DynamicScriptLoaderService {

public loadScript(url: string): Promise<void> {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = (): any => {
resolve();
};
script.onerror = () => {
reject(new Error(`Script load error for ${url}`));
};
document.head.appendChild(script);
});
}

}
8 changes: 4 additions & 4 deletions src/app/general/service/logged-in.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ export class LoggedInGuard implements CanActivate {
constructor(
private authService: AuthService
) {
console.log("LoggedInGuard.constructor")
console.log("LoggedInGuard.constructor");
}

public async canActivate(): Promise<boolean> {
const canActivate = this.authService.checkIfUserAuthenticated()
console.log("LoggedInGuard.canActivate", canActivate);
return canActivate;
const userAuthenticated = await this.authService.checkIfUserAuthenticated();
console.log("LoggedInGuard.canActivate", userAuthenticated);
return userAuthenticated;
}

}
2 changes: 1 addition & 1 deletion src/app/login-home/login-home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</p>
</mat-card-content>
<mat-card-actions align="end">
<button mat-button color="warn" mat-raised-button #btnFocus=matButton [autofocus]="btnFocus.focus()" (click)="handleAuthClick()">Google Login</button>
<button [disabled]="!authService.allowToSignIn" mat-button color="warn" mat-raised-button #btnFocus=matButton [autofocus]="btnFocus.focus()" (click)="handleAuthClick()">Google Login</button>
</mat-card-actions>
</mat-card>
</mat-grid-tile>
Expand Down
Loading