feat: implement OAuth2 authentication flow with Google and PocketId, add callback handling
This commit is contained in:
@@ -13,15 +13,6 @@ import { OAuthService } from 'angular-oauth2-oidc';
|
|||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'Digitálny asistent PAS';
|
title = 'Digitálny asistent PAS';
|
||||||
constructor(private readonly as: OAuthService, private readonly router: Router) {
|
constructor(private readonly as: OAuthService, private readonly router: Router) {
|
||||||
this.as.configure({
|
|
||||||
issuer: 'https://accounts.google.com',
|
|
||||||
redirectUri: window.location.origin + '/login',
|
|
||||||
clientId: '1000025801082-09ikmt61a9c9vbdjhpdab9b0ui3vdnij.apps.googleusercontent.com',
|
|
||||||
dummyClientSecret: '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',
|
|
||||||
scope: 'openid profile email',
|
|
||||||
responseType: 'code',
|
|
||||||
strictDiscoveryDocumentValidation: false,
|
|
||||||
timeoutFactor: 0.01,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { AppMaintenanceComponent } from './maintenance/maintenance.component';
|
|||||||
import { AppSideForgotPasswordComponent } from './side-forgot-password/side-forgot-password.component';
|
import { AppSideForgotPasswordComponent } from './side-forgot-password/side-forgot-password.component';
|
||||||
import { AppSideLoginComponent } from './side-login/side-login.component';
|
import { AppSideLoginComponent } from './side-login/side-login.component';
|
||||||
import { AppSideRegisterComponent } from './side-register/side-register.component';
|
import { AppSideRegisterComponent } from './side-register/side-register.component';
|
||||||
import { AppSideTwoStepsComponent } from './side-two-steps/side-two-steps.component';
|
import { CallbackComponent } from './callback/callback.component';
|
||||||
|
|
||||||
export const AuthenticationRoutes: Routes = [
|
export const AuthenticationRoutes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -52,8 +52,8 @@ export const AuthenticationRoutes: Routes = [
|
|||||||
component: AppSideRegisterComponent,
|
component: AppSideRegisterComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'side-two-steps',
|
path: 'callback',
|
||||||
component: AppSideTwoStepsComponent,
|
component: CallbackComponent,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
27
Web/src/app/pages/authentication/callback/callback.component.html
Executable file
27
Web/src/app/pages/authentication/callback/callback.component.html
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
<div class="blank-layout-container justify-content-center">
|
||||||
|
<div class="position-relative row w-100 h-100">
|
||||||
|
<div class="col-lg-8 col-xl-9 bg-gredient p-0">
|
||||||
|
<div class="p-24 h-100">
|
||||||
|
<app-branding></app-branding>
|
||||||
|
|
||||||
|
<div class="align-items-center justify-content-center img-height d-none d-lg-flex">
|
||||||
|
<img src="/assets/images/backgrounds/login-bg.svg" alt="login" style="max-width: 500px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 col-xl-3 p-0 d-flex justify-content-center">
|
||||||
|
<div class="p-32 d-flex align-items-start align-items-lg-center h-100 max-width-form justify-content-center">
|
||||||
|
<div>
|
||||||
|
<h4 class="f-w-700 f-s-20 m-0">Authentikácia úspešná!</h4>
|
||||||
|
@if(!profile) {
|
||||||
|
<span class="f-s-14 d-block f-s-14 m-t-24">Načítavam Vaše údaje...</span>
|
||||||
|
}
|
||||||
|
@else{
|
||||||
|
<span class="f-s-14 d-block f-s-14 m-t-24">{{profile.email}}</span>
|
||||||
|
<img [src]="profile.picture" alt="profile picture" width="100" class="m-t-16 rounded-circle" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
32
Web/src/app/pages/authentication/callback/callback.component.ts
Executable file
32
Web/src/app/pages/authentication/callback/callback.component.ts
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { CoreService } from '../../../services/core.service';
|
||||||
|
import { MaterialModule } from '../../../material.module';
|
||||||
|
import { BrandingComponent } from '../../../layouts/full/vertical/sidebar/branding.component';
|
||||||
|
import { AuthenticationService } from '../../../services/authentication.service';
|
||||||
|
import { JsonPipe, NgIf } from '@angular/common';
|
||||||
|
import { NgScrollbarModule } from "ngx-scrollbar";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-callback',
|
||||||
|
imports: [RouterModule, MaterialModule, BrandingComponent, JsonPipe, NgIf, NgScrollbarModule],
|
||||||
|
templateUrl: './callback.component.html',
|
||||||
|
})
|
||||||
|
export class CallbackComponent {
|
||||||
|
options: any;
|
||||||
|
profile: any;
|
||||||
|
|
||||||
|
constructor(private settings: CoreService, private as: AuthenticationService) {
|
||||||
|
this.options = this.settings.getOptions();
|
||||||
|
|
||||||
|
// Handle the OAuth2 callback and load user profile
|
||||||
|
this.as
|
||||||
|
.handleCallback()
|
||||||
|
.then(_ => {
|
||||||
|
console.log('Login successful');
|
||||||
|
this.profile = this.as.profile;
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Error handling callback', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,63 +23,16 @@
|
|||||||
Prihlásiť sa pomocou Google
|
Prihlásiť sa pomocou Google
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<!-- <div class="col-12 col-sm-6">
|
|
||||||
<button mat-stroked-button class="w-100 d-flex align-items-center">
|
<a mat-stroked-button class="w-100" style="margin-top:12px" (click)="pocketLogin()">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<img src="/assets/images/svgs/facebook-icon.svg" alt="facebook" width="40" class="m-r-4" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/pocket-id-light.svg"
|
||||||
Sign in with FB
|
alt="google" width="16" class="m-r-8" />
|
||||||
</div>
|
Prihlásiť sa pomocou PocketId
|
||||||
</button>
|
</div>
|
||||||
</div> -->
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- <div class="or-border m-t-30">or sign in with</div>
|
|
||||||
|
|
||||||
<form class="m-t-30" [formGroup]="form" (ngSubmit)="submit()">
|
|
||||||
<mat-label class="f-s-14 f-w-600 m-b-12 d-block">Username</mat-label>
|
|
||||||
<mat-form-field appearance="outline" class="w-100" color="primary">
|
|
||||||
<input matInput formControlName="uname" />
|
|
||||||
@if(f['uname'].touched && f['uname'].invalid) {
|
|
||||||
<mat-hint class="m-b-16 error-msg">
|
|
||||||
@if(f['uname'].errors && f['uname'].errors['required']) {
|
|
||||||
<div class="text-error">Name is required.</div>
|
|
||||||
} @if(f['uname'].errors && f['uname'].errors['minlength']) {
|
|
||||||
<div class="text-error">Name should be 6 character.</div>
|
|
||||||
}
|
|
||||||
</mat-hint>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<!-- password
|
|
||||||
<mat-label class="f-s-14 f-w-600 m-b-12 d-block">Password</mat-label>
|
|
||||||
<mat-form-field appearance="outline" class="w-100" color="primary">
|
|
||||||
<input matInput type="password" formControlName="password" />
|
|
||||||
@if(f['password'].touched && f['password'].invalid) {
|
|
||||||
<mat-hint class="m-b-16 error-msg">
|
|
||||||
@if( f['password'].errors && f['password'].errors['required'])
|
|
||||||
{
|
|
||||||
<div class="text-error">Password is required.</div>
|
|
||||||
}
|
|
||||||
</mat-hint>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<div class="d-flex align-items-center m-b-12">
|
|
||||||
<mat-checkbox color="primary">Remember this Device</mat-checkbox>
|
|
||||||
<a [routerLink]="['/authentication/side-forgot-pwd']"
|
|
||||||
class="text-primary f-w-600 text-decoration-none m-l-auto f-s-14">Forgot Password ?</a>
|
|
||||||
</div>
|
|
||||||
<button mat-flat-button color="primary" class="w-100" [disabled]="!form.valid">
|
|
||||||
Sign In
|
|
||||||
</button>
|
|
||||||
<!-- input
|
|
||||||
</form>
|
|
||||||
<span class="d-block f-w-500 d-block m-t-24">New to Modernize?
|
|
||||||
<a [routerLink]="['/authentication/side-register']"
|
|
||||||
class="text-decoration-none text-primary f-w-500 f-s-14">
|
|
||||||
Create an account</a>
|
|
||||||
</span> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule }
|
|||||||
import { Router, RouterModule } from '@angular/router';
|
import { Router, RouterModule } from '@angular/router';
|
||||||
import { MaterialModule } from '../../../material.module';
|
import { MaterialModule } from '../../../material.module';
|
||||||
import { BrandingComponent } from '../../../layouts/full/vertical/sidebar/branding.component';
|
import { BrandingComponent } from '../../../layouts/full/vertical/sidebar/branding.component';
|
||||||
import { OAuthService } from 'angular-oauth2-oidc';
|
import { AuthenticationService } from '../../../services/authentication.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-side-login',
|
selector: 'app-side-login',
|
||||||
@@ -14,30 +14,22 @@ import { OAuthService } from 'angular-oauth2-oidc';
|
|||||||
export class AppSideLoginComponent {
|
export class AppSideLoginComponent {
|
||||||
options: any;
|
options: any;
|
||||||
|
|
||||||
constructor(private settings: CoreService, private router: Router, private readonly as: OAuthService) {
|
constructor(private settings: CoreService, private router: Router, private readonly as: AuthenticationService) {
|
||||||
this.options = this.settings.getOptions();
|
this.options = this.settings.getOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
form = new FormGroup({
|
|
||||||
uname: new FormControl('', [Validators.required, Validators.minLength(6)]),
|
|
||||||
password: new FormControl('', [Validators.required]),
|
|
||||||
});
|
|
||||||
|
|
||||||
get f() {
|
|
||||||
return this.form.controls;
|
|
||||||
}
|
|
||||||
|
|
||||||
googleLogin() {
|
googleLogin() {
|
||||||
console.warn('Google login initiated');
|
this.as.configureAndLogin({
|
||||||
this.as.loadDiscoveryDocumentAndLogin()
|
issuer: 'https://accounts.google.com',
|
||||||
.then(_ =>{}
|
clientId: '1000025801082-09ikmt61a9c9vbdjhpdab9b0ui3vdnij.apps.googleusercontent.com',
|
||||||
// this.as.initLoginFlowInPopup()
|
dummyClientSecret: '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',
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pocketLogin() {
|
||||||
submit() {
|
this.as.configureAndLogin({
|
||||||
// console.log(this.form.value);
|
issuer: 'https://identity.lesko.me',
|
||||||
this.router.navigate(['/dashboards/dashboard1']);
|
clientId: '21131567-fea1-42a2-8907-21abd874eff8',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
Web/src/app/services/authentication.service.ts
Normal file
116
Web/src/app/services/authentication.service.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
|
||||||
|
|
||||||
|
const CONFIG_KEY = 'oauth_config_v1';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AuthenticationService {
|
||||||
|
|
||||||
|
private config: Partial<AuthConfig> = {
|
||||||
|
redirectUri: window.location.origin + '/authentication/callback',
|
||||||
|
scope: 'openid profile email',
|
||||||
|
responseType: 'code',
|
||||||
|
requireHttps: false,
|
||||||
|
strictDiscoveryDocumentValidation: false,
|
||||||
|
timeoutFactor: 0.01,
|
||||||
|
};
|
||||||
|
|
||||||
|
public profile: any = null;
|
||||||
|
|
||||||
|
constructor(private oauthService: OAuthService) { }
|
||||||
|
|
||||||
|
saveConfig(cfg: Partial<AuthConfig>) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(CONFIG_KEY, JSON.stringify(cfg));
|
||||||
|
} catch {
|
||||||
|
// ignore}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfig(): Partial<AuthConfig> | null {
|
||||||
|
const raw = localStorage.getItem(CONFIG_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as Partial<AuthConfig>;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Configure the library and persist configuration
|
||||||
|
configure(cfg: Partial<AuthConfig>) {
|
||||||
|
this.config = { ...this.config, ...cfg };
|
||||||
|
this.saveConfig(this.config);
|
||||||
|
this.oauthService.configure(this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure and immediately start login flow
|
||||||
|
configureAndLogin(cfg: Partial<AuthConfig>) {
|
||||||
|
this.configure(cfg);
|
||||||
|
this.startLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore configuration from storage and apply to OAuthService
|
||||||
|
restoreConfiguration(): boolean {
|
||||||
|
const cfg = this.loadConfig() ?? this.config;
|
||||||
|
if (!cfg) return false;
|
||||||
|
this.oauthService.configure(cfg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start login flow using discovery document + Authorization Code (PKCE)
|
||||||
|
startLogin(cfg?: Partial<AuthConfig>): Promise<void> {
|
||||||
|
if (cfg) this.configure(cfg);
|
||||||
|
return this.oauthService
|
||||||
|
.loadDiscoveryDocument()
|
||||||
|
.then(() => {
|
||||||
|
// initCodeFlow will redirect to the provider
|
||||||
|
this.oauthService.initCodeFlow();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call this on the callback route to process the redirect and obtain tokens + profile
|
||||||
|
handleCallback(): Promise<any> {
|
||||||
|
this.restoreConfiguration();
|
||||||
|
// Ensure discovery document loaded, then process code flow, then load profile
|
||||||
|
return this.oauthService
|
||||||
|
.loadDiscoveryDocumentAndTryLogin()
|
||||||
|
.then((isLoggedIn: boolean) => {
|
||||||
|
if (!isLoggedIn && !this.oauthService.hasValidAccessToken()) {
|
||||||
|
return Promise.reject('No valid token after callback');
|
||||||
|
}
|
||||||
|
return this.oauthService.loadUserProfile();
|
||||||
|
}).then(profile => {
|
||||||
|
this.profile = profile["info"];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience helpers
|
||||||
|
get events() {
|
||||||
|
return this.oauthService.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasValidAccessToken(): boolean {
|
||||||
|
return this.oauthService.hasValidAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccessToken(): string {
|
||||||
|
return this.oauthService.getAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdToken(): string {
|
||||||
|
return this.oauthService.getIdToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdentityClaims(): object | null {
|
||||||
|
return this.oauthService.getIdentityClaims() as object | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(destroyLocalSession = false) {
|
||||||
|
if (destroyLocalSession) {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(CONFIG_KEY);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
this.oauthService.logOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user