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 {
|
||||
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,
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
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,62 +23,15 @@
|
||||
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">
|
||||
|
||||
<a mat-stroked-button class="w-100" style="margin-top:12px" (click)="pocketLogin()">
|
||||
<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
|
||||
<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>
|
||||
</button>
|
||||
</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>
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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