ソースを参照

webautnn implemnentation

Dr-Swopt 1 ヶ月 前
コミット
96c220c736

+ 2 - 2
angular.json

@@ -37,8 +37,8 @@
               "budgets": [
                 {
                   "type": "initial",
-                  "maximumWarning": "1MB",
-                  "maximumError": "2MB"
+                  "maximumWarning": "2MB",
+                  "maximumError": "4MB"
                 },
                 {
                   "type": "anyComponentStyle",

+ 23 - 0
certs/cert.pem

@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIIDzzCCAregAwIBAgIUHzwMiYlmu5EqL5/VXfpMa9ZK7owwDQYJKoZIhvcNAQEL
+BQAwdzELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9u
+ZG9uMQ4wDAYDVQQKDAVTd29wdDEMMAoGA1UECwwDTk9OMQwwCgYDVQQDDANQT1Ax
+GjAYBgkqhkiG9w0BCQEWC3BvcEBwb3AuY29tMB4XDTI1MDkyNjAyNDkwOFoXDTI2
+MDkyNjAyNDkwOFowdzELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRvbjEPMA0G
+A1UEBwwGTG9uZG9uMQ4wDAYDVQQKDAVTd29wdDEMMAoGA1UECwwDTk9OMQwwCgYD
+VQQDDANQT1AxGjAYBgkqhkiG9w0BCQEWC3BvcEBwb3AuY29tMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnJdca7WhpwQfNrXh5oJkYRC3bS0dMTumhgok
+LlainIR8JD0AlG/d/70wBNwSqablbtZS40nb5OSLve47N50FnB0e4C+43ZRkO9BW
+Ip7VpVtIeHsNwk3UgP4rfg2BBLE6FMM0t515kWuNyeWPE8/I1se3/kU7wQj2kmji
+XGPjwBZr6x7K89qh+xPWKDfcPyWl3PObb3ZxqUOt86HdUFcangA/iTIKW4FJ2vzq
+mnG6FyDWnQjuWuZj8C5/BG02XHBC0hyR41eY+T9sb/632++kLqK2KJfH7lZhB0Lr
+eW3p2Fxyu8hBP5K/RzQijGiVFFRYFzxIpe3ekrE2E3kK53pTsQIDAQABo1MwUTAd
+BgNVHQ4EFgQU4WiDGcKu4aNl4QIBWGxYuD2H448wHwYDVR0jBBgwFoAU4WiDGcKu
+4aNl4QIBWGxYuD2H448wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
+AQEAmSswPOTcRU990N31wqEy5N2YGMchCZueZBaQBQbdfY1u6WkziMjuv3xu4koE
+AZVLRqQ6AmxUEMRKfbT1B3dq7Bmjs/ZitKpI7F3uxsusUyGxSgoU6ghIJqHhtKQk
++5f0WON7KSqdOzlere7wXyTUPzhllmLtfZ1vRodH5Hy8atA3M5YqnQYJp33FQQb8
++W3yP9eK55vekG9tGgye7P5qNwckWH7pNM3xCmXjhOfipu8RCZb2oAHWS/kX3GKQ
+gSKKTlsCp1bM9sQS85XnC86AZh66AGOYyuQ93/UlqKClhxq6Et4YO1qGhrQKFNCf
+TrULGhulJTYW7aB+2jce5c/iKA==
+-----END CERTIFICATE-----

+ 28 - 0
certs/key.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcl1xrtaGnBB82
+teHmgmRhELdtLR0xO6aGCiQuVqKchHwkPQCUb93/vTAE3BKppuVu1lLjSdvk5Iu9
+7js3nQWcHR7gL7jdlGQ70FYintWlW0h4ew3CTdSA/it+DYEEsToUwzS3nXmRa43J
+5Y8Tz8jWx7f+RTvBCPaSaOJcY+PAFmvrHsrz2qH7E9YoN9w/JaXc85tvdnGpQ63z
+od1QVxqeAD+JMgpbgUna/OqacboXINadCO5a5mPwLn8EbTZccELSHJHjV5j5P2xv
+/rfb76QuorYol8fuVmEHQut5benYXHK7yEE/kr9HNCKMaJUUVFgXPEil7d6SsTYT
+eQrnelOxAgMBAAECggEAIrMNyVZp2GM68XozcUuCp9m288vz9JV1zK0RoG0VQp6/
+DZ6w8rOpW4LWUbVcs1hm7f0zR+d1gU69wpw4ZhX3CMWRMneQvRUlcvWzKJ/O0wIb
+2IrYHS73AJCSHbArX1lQeApbs7aDGlzdghhK8MZyCFogZQl9eMSeMwpn4rZF4Sod
+5itEfXrv7H01KZbT6KdPXJ0sbfez6hImeQiL3TJlKACCOoTp0PkKMXP4H6238zL5
+UZCCA0wyhsUG3/i3Lyr7yLdkLk7zLFLEJ4FD3cP4CvhFyiBRUy0usJnpR0lzS+N1
+hYDT4mGPzYafjWKYvTYa1m8BVwX4npQdC008L405uwKBgQDT6QNm37wxhpHe43FP
+jNIH7uJYBBpYT9Jm2MQbaGOKiOe6HyZ5KweYeYbX9YuGFqDaybMwRDDPSbsJOZDA
+JwJjG1bVCbXozLQGuaeKeRGH6DENS+Fsip0rPJkdHAG+S1B9g/crp2ZC22gZxB94
+03cG8QkftUi0TyuQymQx+fxxBwKBgQC9K+QLq7OpE7PDBfxHezWa88K2iQ0ubWwn
+9lCK9LNY5nwkBSHaAwrNWy7Pnt1OwO0yT/v69gPKIjKGh7KViQ+5fAiOyP0WuBHc
+2po1l2bDwha40S6NmjzE1h6edTUOJo3XW2+4FX/sjxSUiRrYDgLRvlSoIg8tIJ1n
+4WYDz6Y/hwKBgDe8IIXtMJ1CDJm37nSC0Db/8I4/vgIeNHOSbbnbsdqc+X2tdbwG
+wj+rLvkb/u9sgjApPrTiKohKlyPs/RJc8DbK2QK9RBgPxwXBzLwR7bd2LXiWzZpz
+trTJgmfyls7LFkd87wSPSckp1e548+IelD7CJKkvUAkEjavOX535Zxj/AoGAGa8V
+3UfIstIL/BSZ9hKSaqFh9GqTMZSFtL9KnDMxDobsn+9ac0EqfEs/Bc1p+sFS8xvM
++Hvic9VEyuMtqgPb8LEYcFp1kloXgsbjXRdbSoTVlO5BxdQFICx6J8V+GJe/dlfh
+yTqSDco3XxtmW6M7WsLet504NkZRWMNPmIDe740CgYEAgZSZWlxkSxZWVbpYxEtc
+X/UWg/URsUSievUBdlhk9w9H+6z36JPwcOiOnmR/D6gA1ssXVNsbDur3KX27wc0H
+X6sDw1427DJLm6DtfTCLDLRoh/JpzpN//1Frt+PgHD/scETI229Q0PWf3Vne7fK3
+wUzA2EtvI5QbV6+Xb7B8Oso=
+-----END PRIVATE KEY-----

+ 2 - 1
package.json

@@ -5,6 +5,7 @@
     "ng": "ng",
     "start": "ng serve",
     "build": "ng build",
+    "cert": "ng serve --ssl true --ssl-key ../certs/key.pem --ssl-cert ./certs/cert.pem --host 0.0.0.0",
     "nest": "ng build --configuration production --base-href /",
     "watch": "ng build --watch --configuration development",
     "test": "ng test"
@@ -46,4 +47,4 @@
     "karma-jasmine-html-reporter": "~2.1.0",
     "typescript": "~5.8.3"
   }
-}
+}

+ 0 - 4
src/app/app.routes.ts

@@ -1,16 +1,12 @@
 // app.routes.ts
 import { Routes } from '@angular/router';
-import { LoginComponent } from './login/login.component';
 import { DashboardComponent } from './dashboard/dashboard.component';
 import { authGuard } from './services/auth.guard';
-import { RegisterComponent } from './register/register.component';
 import { WebauthnRegisterComponent } from './webauthn-register/webauthn-register.component';
 import { WebauthnLoginComponent } from './webauthn-login/webauthn-login.component';
 
 export const routes: Routes = [
   { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
-  { path: 'login', component: LoginComponent },
-  { path: 'register', component: RegisterComponent },
   { path: 'webauthn-register', component: WebauthnRegisterComponent },
   { path: 'webauthn-login', component: WebauthnLoginComponent },
   { path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },

+ 3 - 0
src/app/config.ts

@@ -0,0 +1,3 @@
+export const webConfig = {
+    exposedUrl: `https://192.168.100.100:4200`,
+}

+ 0 - 12
src/app/login/login.component.css

@@ -1,12 +0,0 @@
-.login-card {
-  max-width: 400px;
-  margin: 5rem auto;
-  padding: 2rem;
-  display: flex;
-  flex-direction: column;
-  gap: 1rem;
-}
-
-.full-width {
-  width: 100%;
-}

+ 0 - 33
src/app/login/login.component.html

@@ -1,33 +0,0 @@
-<mat-card class="login-card">
-  <h2>Login</h2>
-
-  <form [formGroup]="form" (ngSubmit)="onSubmit()">
-    <mat-form-field appearance="outline" class="full-width">
-      <mat-label>Email</mat-label>
-      <input matInput formControlName="email" type="email" required />
-    </mat-form-field>
-
-    <mat-form-field appearance="outline" class="full-width">
-      <mat-label>Password</mat-label>
-      <input matInput formControlName="password" type="password" required />
-    </mat-form-field>
-
-    <button mat-raised-button color="primary" type="submit" class="full-width">
-      Login
-    </button>
-
-    <button mat-button type="button" class="full-width" (click)="goToRegister()">
-      Register
-    </button>
-
-    <button mat-stroked-button color="accent" type="button" class="full-width" (click)="goToWebauthnRegister()">
-      Register with Passkey
-    </button>
-
-    <mat-divider class="my-3"></mat-divider>
-
-    <button mat-flat-button color="warn" type="button" class="full-width" (click)="loginWithWebauthn()">
-      Login with Passkey
-    </button>
-  </form>
-</mat-card>

+ 0 - 23
src/app/login/login.component.spec.ts

@@ -1,23 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { LoginComponent } from './login.component';
-
-describe('LoginComponent', () => {
-  let component: LoginComponent;
-  let fixture: ComponentFixture<LoginComponent>;
-
-  beforeEach(async () => {
-    await TestBed.configureTestingModule({
-      imports: [LoginComponent]
-    })
-    .compileComponents();
-
-    fixture = TestBed.createComponent(LoginComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});

+ 0 - 75
src/app/login/login.component.ts

@@ -1,75 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
-import { CommonModule } from '@angular/common';
-import { Router, RouterModule } from '@angular/router';
-import { AuthService } from '../services/auth.service';
-
-import { MatFormFieldModule } from '@angular/material/form-field';
-import { MatInputModule } from '@angular/material/input';
-import { MatButtonModule } from '@angular/material/button';
-import { MatCardModule } from '@angular/material/card';
-import { MatDividerModule } from '@angular/material/divider';
-
-@Component({
-  selector: 'app-login',
-  standalone: true,
-  imports: [
-    CommonModule,
-    ReactiveFormsModule,
-    RouterModule,
-    MatFormFieldModule,
-    MatInputModule,
-    MatButtonModule,
-    MatCardModule,
-    MatDividerModule
-  ],
-  templateUrl: './login.component.html',
-  styleUrls: ['./login.component.css']
-})
-export class LoginComponent implements OnInit {
-  form!: FormGroup;
-
-  constructor(
-    private fb: FormBuilder,
-    private auth: AuthService,
-    private router: Router
-  ) { }
-
-  ngOnInit(): void {
-    this.form = this.fb.group({
-      email: ['', [Validators.required, Validators.email]],
-      password: ['', Validators.required]
-    });
-  }
-
-  goToRegister(): void {
-    this.router.navigate(['/register']);
-  }
-
-  goToWebauthnRegister(): void {
-    this.router.navigate(['/webauthn-register']);
-  }
-
-  loginWithWebauthn(): void {
-    this.router.navigate(['/webauthn-login'])
-  }
-
-  onSubmit(): void {
-    if (this.form.invalid) {
-      this.form.markAllAsTouched();
-      return;
-    }
-
-    this.auth.login(this.form.value).subscribe({
-      next: (res) => {
-        this.auth.setUserName(res.name)
-        this.auth.storeToken(res.access_token);
-        this.router.navigate(['/dashboard']);
-      },
-      error: (err) => {
-        console.error('Login failed', err);
-        alert('Login failed: ' + (err.error?.message || 'Unknown error'));
-      }
-    });
-  }
-}

+ 0 - 12
src/app/register/register.component.css

@@ -1,12 +0,0 @@
-.register-card {
-  max-width: 400px;
-  margin: 5rem auto;
-  padding: 2rem;
-  display: flex;
-  flex-direction: column;
-  gap: 1rem;
-}
-
-.full-width {
-  width: 100%;
-}

+ 0 - 23
src/app/register/register.component.html

@@ -1,23 +0,0 @@
-<mat-card class="register-card">
-  <h2>Register</h2>
-
-  <form [formGroup]="form" (ngSubmit)="onSubmit()">
-    <mat-form-field appearance="outline" class="full-width">
-      <mat-label>Name</mat-label>
-      <input matInput formControlName="name" />
-    </mat-form-field>
-
-    <mat-form-field appearance="outline" class="full-width">
-      <mat-label>Email</mat-label>
-      <input matInput formControlName="email" type="email" />
-    </mat-form-field>
-
-    <mat-form-field appearance="outline" class="full-width">
-      <mat-label>Password</mat-label>
-      <input matInput formControlName="password" type="password" />
-    </mat-form-field>
-
-    <button mat-raised-button color="primary" type="submit" class="full-width">Register</button>
-    <button mat-button type="button" class="full-width" (click)="router.navigate(['/login'])">Back to Login</button>
-  </form>
-</mat-card>

+ 0 - 23
src/app/register/register.component.spec.ts

@@ -1,23 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { RegisterComponent } from './register.component';
-
-describe('RegisterComponent', () => {
-  let component: RegisterComponent;
-  let fixture: ComponentFixture<RegisterComponent>;
-
-  beforeEach(async () => {
-    await TestBed.configureTestingModule({
-      imports: [RegisterComponent]
-    })
-    .compileComponents();
-
-    fixture = TestBed.createComponent(RegisterComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});

+ 0 - 64
src/app/register/register.component.ts

@@ -1,64 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
-import { Router } from '@angular/router';
-import { AuthService } from '../services/auth.service';
-
-import { MatFormFieldModule } from '@angular/material/form-field';
-import { MatInputModule } from '@angular/material/input';
-import { MatButtonModule } from '@angular/material/button';
-import { MatCardModule } from '@angular/material/card';
-
-@Component({
-  standalone: true,
-  selector: 'app-register',
-  imports: [
-    CommonModule,
-    ReactiveFormsModule,
-    MatFormFieldModule,
-    MatInputModule,
-    MatButtonModule,
-    MatCardModule
-  ],
-  templateUrl: './register.component.html',
-  styleUrls: ['./register.component.css']
-})
-export class RegisterComponent implements OnInit {
-  form!: FormGroup;
-
-  constructor(
-    private fb: FormBuilder,
-    public router: Router,
-    private auth: AuthService
-  ) { }
-
-  ngOnInit(): void {
-    this.form = this.fb.group({
-      name: ['', Validators.required],
-      email: ['', [Validators.required, Validators.email]],
-      password: ['', [Validators.required, Validators.minLength(6)]]
-    });
-  }
-
-  onSubmit(): void {
-    if (this.form.invalid) {
-      this.form.markAllAsTouched();
-      return;
-    }
-
-    const formValue = this.form.value;
-
-    this.auth.register(formValue).subscribe({
-      next: (res) => {
-        console.log(res)
-        this.auth.setUserName(res.name)
-        this.auth.storeToken(res.access_token);
-        this.router.navigate(['/dashboard']);
-      },
-      error: (err) => {
-        console.error('Registration failed', err);
-        alert('Registration failed: ' + (err.error?.message || 'Unknown error'));
-      }
-    });
-  }
-}

+ 1 - 1
src/app/services/auth.guard.ts

@@ -12,6 +12,6 @@ export const authGuard: CanActivateFn = () => {
     return true;
   }
 
-  router.navigate(['/login']);
+  router.navigate(['/webauthn-login']);
   return false;
 };

+ 37 - 2
src/app/services/auth.service.ts

@@ -5,10 +5,11 @@ import { Observable } from 'rxjs';
 import { AuthResponse, LoginPayload, RegisterPayload } from '../interfaces/interface';
 import { AuthenticationResponseJSON, RegistrationResponseJSON, startAuthentication, startRegistration } from '@simplewebauthn/browser';
 import { A11yModule } from '@angular/cdk/a11y';
+import { webConfig } from '../config';
 
 @Injectable({ providedIn: 'root' })
 export class AuthService {
-  private readonly baseUrl = 'https://b8c8-115-132-229-66.ngrok-free.app/api';
+  private readonly baseUrl = webConfig.exposedUrl + `/api`
   private readonly tokenKey = 'auth_token';
   private userName!: string
 
@@ -108,6 +109,40 @@ export class AuthService {
     });
   }
 
+  // login via differnt devices.
+  async webauthnPasskeyLogin(): Promise<AuthResponse> {
+    return new Promise(async (resolve, reject) => {
+      try {
+        // 1. Request generic passkey login options from backend
+        const options = await this.http
+          .post<any>(`${this.baseUrl}/auth/webauthn-login-options`, { passkey: true })
+          .toPromise();
+
+        if (!options) throw new Error('Login options not received');
+
+        // 2. Start WebAuthn login
+        const assertionResponse: AuthenticationResponseJSON = await startAuthentication({
+          optionsJSON: options,
+        });
+
+        // 3. Send only the credential back — backend must resolve user from credential ID
+        const res = await this.http
+          .post<AuthResponse>(`${this.baseUrl}/auth/webauthn-login`, assertionResponse)
+          .toPromise();
+
+        if (!res?.access_token) {
+          reject(new Error('Invalid login response'));
+        } else {
+          this.storeToken(res.access_token);
+          this.setUserName(res.name);
+          resolve(res);
+        }
+      } catch (err) {
+        reject(err);
+      }
+    });
+  }
+
   reportAttendance(url: string): Observable<AuthResponse> {
     let payload = {
       name: this.userName,
@@ -151,7 +186,7 @@ export class AuthService {
 
   logout(): void {
     localStorage.removeItem(this.tokenKey);
-    this.router.navigate(['/login']);
+    this.router.navigate(['/webauthn-login']);
   }
 
   getServerUrl(): Observable<string> {

+ 20 - 16
src/app/webauthn-login/webauthn-login.component.html

@@ -1,21 +1,25 @@
 <mat-card class="login-card">
-  <h2>Login with Passkey</h2>
+  <h2>Login</h2>
 
-  <form (ngSubmit)="loginWithPasskey()">
-    <mat-form-field appearance="outline" class="full-width">
-      <mat-label>Email</mat-label>
-      <input matInput [(ngModel)]="email" name="email" required />
-    </mat-form-field>
+  <button
+    mat-raised-button
+    color="accent"
+    class="full-width"
+    (click)="loginWithPasskey()"
+    [disabled]="loading"
+  >
+    Login with Passkey
+  </button>
 
-    <button mat-raised-button color="primary" type="submit" class="full-width" [disabled]="loading || !email">
-      Login with Passkey
-    </button>
+  <button
+    mat-button
+    type="button"
+    class="full-width"
+    (click)="goToRegistration()"
+  >
+    Register Account
+  </button>
 
-    <button mat-button type="button" class="full-width" (click)="goBack()">
-      Back to Login
-    </button>
-
-    <mat-hint *ngIf="message">{{ message }}</mat-hint>
-    <mat-error *ngIf="errorMessage">{{ errorMessage }}</mat-error>
-  </form>
+  <mat-hint *ngIf="message">{{ message }}</mat-hint>
+  <mat-error *ngIf="errorMessage">{{ errorMessage }}</mat-error>
 </mat-card>

+ 12 - 6
src/app/webauthn-login/webauthn-login.component.ts

@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 import { startAuthentication } from '@simplewebauthn/browser';
 import { AuthService } from '../services/auth.service';
 import { Router, RouterModule } from '@angular/router';
@@ -27,31 +27,37 @@ import { MatDividerModule } from '@angular/material/divider';
   templateUrl: './webauthn-login.component.html',
   styleUrl: './webauthn-login.component.css'
 })
-export class WebauthnLoginComponent {
+export class WebauthnLoginComponent implements OnInit {
   email = '';
   message = '';
   errorMessage = '';
   loading = false;
+  passkeySupportChecked = false;
 
   constructor(private authService: AuthService, private router: Router) { }
 
+  ngOnInit(): void {
+
+  }
+
   async loginWithPasskey() {
     this.message = '';
     this.errorMessage = '';
     this.loading = true;
 
     try {
-      const res = await this.authService.webauthnLogin(this.email);
+      const res = await this.authService.webauthnPasskeyLogin(); // ✅ new method
       this.message = 'Login successful!';
       this.router.navigate(['/dashboard']);
     } catch (err: any) {
-      this.errorMessage = err?.message || 'Login failed';
+      this.errorMessage = err?.message || 'Login with passkey failed';
     } finally {
       this.loading = false;
     }
   }
 
-  goBack() {
-    this.router.navigate(['/login']);
+
+  goToRegistration() {
+    this.router.navigate(['/webauthn-register']);
   }
 }

+ 1 - 1
src/app/webauthn-register/webauthn-register.component.ts

@@ -52,6 +52,6 @@ export class WebauthnRegisterComponent {
   }
 
   goBack() {
-    this.router.navigate(['/login']);
+    this.router.navigate(['/webauthn-login']);
   }
 }