feat: implement OAuth2 authentication flow with Google and PocketId, add callback handling

This commit is contained in:
Marek Lesko
2025-10-28 19:43:46 +00:00
parent 52696196b2
commit ed5cba4677
7 changed files with 200 additions and 89 deletions

View File

@@ -13,15 +13,6 @@ import { OAuthService } from 'angular-oauth2-oidc';
export class AppComponent {
title = 'Digitálny asistent PAS';
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,
});
}
}

View File

@@ -9,7 +9,7 @@ import { AppMaintenanceComponent } from './maintenance/maintenance.component';
import { AppSideForgotPasswordComponent } from './side-forgot-password/side-forgot-password.component';
import { AppSideLoginComponent } from './side-login/side-login.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 = [
{
@@ -52,8 +52,8 @@ export const AuthenticationRoutes: Routes = [
component: AppSideRegisterComponent,
},
{
path: 'side-two-steps',
component: AppSideTwoStepsComponent,
path: 'callback',
component: CallbackComponent,
},
],
},

View 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>

View 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);
});
}
}

View File

@@ -23,63 +23,16 @@
Prihlásiť sa pomocou Google
</div>
</a>
<!-- <div class="col-12 col-sm-6">
<button mat-stroked-button class="w-100 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" />
Sign in with FB
</div>
</button>
</div> -->
<a mat-stroked-button class="w-100" style="margin-top:12px" (click)="pocketLogin()">
<div class="d-flex align-items-center">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/pocket-id-light.svg"
alt="google" width="16" class="m-r-8" />
Prihlásiť sa pomocou PocketId
</div>
</a>
</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>

View File

@@ -4,7 +4,7 @@ import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule }
import { Router, RouterModule } from '@angular/router';
import { MaterialModule } from '../../../material.module';
import { BrandingComponent } from '../../../layouts/full/vertical/sidebar/branding.component';
import { OAuthService } from 'angular-oauth2-oidc';
import { AuthenticationService } from '../../../services/authentication.service';
@Component({
selector: 'app-side-login',
@@ -14,30 +14,22 @@ import { OAuthService } from 'angular-oauth2-oidc';
export class AppSideLoginComponent {
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();
}
form = new FormGroup({
uname: new FormControl('', [Validators.required, Validators.minLength(6)]),
password: new FormControl('', [Validators.required]),
});
get f() {
return this.form.controls;
}
googleLogin() {
console.warn('Google login initiated');
this.as.loadDiscoveryDocumentAndLogin()
.then(_ =>{}
// this.as.initLoginFlowInPopup()
);
this.as.configureAndLogin({
issuer: 'https://accounts.google.com',
clientId: '1000025801082-09ikmt61a9c9vbdjhpdab9b0ui3vdnij.apps.googleusercontent.com',
dummyClientSecret: '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',
});
}
submit() {
// console.log(this.form.value);
this.router.navigate(['/dashboards/dashboard1']);
pocketLogin() {
this.as.configureAndLogin({
issuer: 'https://identity.lesko.me',
clientId: '21131567-fea1-42a2-8907-21abd874eff8',
});
}
}

View 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();
}
}