Skip to content

Commit

Permalink
#7: local oidc server
Browse files Browse the repository at this point in the history
  • Loading branch information
ga-ebp committed Mar 13, 2024
1 parent fd1d63e commit 5a31f4f
Show file tree
Hide file tree
Showing 13 changed files with 429 additions and 47 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The following steps must be carried out once:
| pgAdmin (docker) | [localhost:5051](http://localhost:5051/) | .env `$PGADMIN_EMAIL` |.env `$PGADMIN_PASSWORD` |
| MinIO (docker) | [localhost:9001](http://localhost:9001/) | .env `$STORAGE_USER` |.env `$STORAGE_PASSWORD` |
| smtp4dev (docker) | [localhost:5000](http://localhost:5000/) | n/a | n/a |
| oidc-server (docker) | [localhost:4011](http://localhost:4011/) | n/a | n/a |

### Creating elastic-search index

Expand Down
4 changes: 4 additions & 0 deletions apps/client-asset-sg/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { assert } from 'tsafe';

import { AppPortalService, setCssCustomProperties } from '@asset-sg/client-shared';
import { FavouriteService } from '@asset-sg/favourite';
import { AuthService } from '@asset-sg/auth';

Check failure on line 9 in apps/client-asset-sg/src/app/app.component.ts

View workflow job for this annotation

GitHub Actions / Build and run tests

`@asset-sg/auth` import should occur before import of `@asset-sg/client-shared`

const fullHdWidth = 1920;

Expand All @@ -20,8 +21,11 @@ export class AppComponent {

public appPortalService = inject(AppPortalService);
private _favouriteService = inject(FavouriteService);
private _authService = inject(AuthService);

constructor() {
this._authService.init();

const wndw = this._wndw;
assert(wndw != null);

Expand Down
7 changes: 4 additions & 3 deletions apps/client-asset-sg/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { PushModule } from '@rx-angular/template/push';
import * as O from 'fp-ts/Option';
import * as C from 'io-ts/Codec';

import { AuthInterceptor } from '@asset-sg/auth';
import { AuthInterceptor, AuthModule } from '@asset-sg/auth';
import {
AnchorComponent,
ButtonComponent,
Expand Down Expand Up @@ -103,11 +103,12 @@ registerLocaleData(locale_deCH, 'de-CH');
AnchorComponent,
ButtonComponent,
DialogModule,
A11yModule,
A11yModule,
AuthModule,
],
providers: [
provideSvgIcons(icons),
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
// { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'fill', floatLabel: 'auto' } },
{ provide: CURRENT_LANG, useFactory: currentLangFactory },
],
Expand Down
19 changes: 16 additions & 3 deletions apps/server-asset-sg/src/app/jwt/jwt-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HttpException, Injectable, NestMiddleware } from '@nestjs/common';

Check failure on line 1 in apps/server-asset-sg/src/app/jwt/jwt-middleware.ts

View workflow job for this annotation

GitHub Actions / Build and run tests

There should be no empty line within import group

import { NextFunction, Request, Response } from 'express';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
Expand All @@ -8,16 +9,23 @@ import { isTruthy } from '@asset-sg/core';

import { AuthenticatedRequest } from '../models/request';

import { jwtFromCookie } from './jwt';
import { jwtFromCookie, jwtFromToken } from './jwt';

export const cookieKey = 'asset-sg-access-token';

@Injectable()
export class JwtMiddleware implements NestMiddleware {
use(req: Request, _res: Response, next: NextFunction) {

async use(req: Request, _res: Response, next: NextFunction) {
const jwtSecret = process.env['GOTRUE_JWT_SECRET'];
assert(isTruthy(jwtSecret), 'GOTRUE_JWT_SECRET is not defined');
const result = pipe(req.headers.cookie, jwtFromCookie(cookieKey, jwtSecret));

// TODO validate token
//const result = pipe(req.headers.cookie, jwtFromCookie(cookieKey, jwtSecret));
const token = this.extractTokenFromHeader(req);
const decoded = jwtFromToken(token!, jwtSecret);
const result = E.right({ accessToken: token!, jwtPayload: decoded!.payload as any});

if (E.isRight(result)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(req as AuthenticatedRequest).accessToken = result.right.accessToken;
Expand All @@ -27,4 +35,9 @@ export class JwtMiddleware implements NestMiddleware {
throw new HttpException('Unauthorised', 401);
}
}

private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
9 changes: 9 additions & 0 deletions apps/server-asset-sg/src/app/jwt/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ import * as R from 'fp-ts/ReadonlyRecord';
import * as D from 'io-ts/Decoder';
import * as jwt from 'jsonwebtoken';

export const jwtFromToken = (token: string, jwtSecret: string) => {
const decoded = jwt.decode(token, { complete: true });
console.log('decoded', decoded);
/*if(decoded?.payload){
console.log('verify', jwt.verify(token, jwtSecret, { algorithms: ['RS256'] }));
}*/
return decoded;
}

export const jwtFromCookie = (cookieKey: string, jwtSecret: string) => (reqHeaderCookie: unknown) =>
pipe(
reqHeaderCookie,
Expand Down
36 changes: 35 additions & 1 deletion development/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,38 @@ services:
volumes:
- ./volumes/smtp4dev/data:/smtp4dev
environment:
- ServerOptions__HostName=smtp4dev
- ServerOptions__HostName=smtp4dev

oidc-server:
container_name: swissgeol-assets-oidc
image: soluto/oidc-server-mock
restart: unless-stopped
ports:
- "4011:80"
environment:
CLIENTS_CONFIGURATION_PATH: /tmp/config/clients-config.json
USERS_CONFIGURATION_PATH: /tmp/config/users-config.json
IDENTITY_RESOURCES_INLINE: |
[
{
"Name": "local_groups_scope",
"ClaimTypes": [
"local_groups_claim"
]
}
]
SERVER_OPTIONS_INLINE: |
{
"IssuerUri": "http://localhost:4011",
"AccessTokenJwtType": "JWT",
"Discovery": {
"ShowKeySet": true
},
"Authentication": {
"CookieSameSiteMode": "Lax",
"CheckSessionCookieSameSiteMode": "Lax"
}
}
volumes:
- ./init/oidc/oidc-mock-clients.json:/tmp/config/clients-config.json:ro
- ./init/oidc/oidc-mock-users.json:/tmp/config/users-config.json:ro
29 changes: 29 additions & 0 deletions development/init/oidc/oidc-mock-clients.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[{
"ClientId": "assets-client",
"Description": "Client for Authorization Code flow with PKCE",
"RequireClientSecret": false,
"AlwaysIncludeUserClaimsInIdToken": true,
"AllowedGrantTypes": [
"authorization_code"
],
"AllowedResponseTypes": [
"code",
"id_token"
],
"AllowAccessTokensViaBrowser": true,
"RedirectUris": [
"http://localhost:4200"
],
"PostLogoutRedirectUris" : [
"http://localhost:4200"
],
"AllowedScopes": [
"openid",
"profile",
"local_groups_scope"
],
"AccessTokenType": "JWT",
"IdentityTokenLifetime": 3600,
"AccessTokenLifetime": 3600
}
]
76 changes: 76 additions & 0 deletions development/init/oidc/oidc-mock-users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
[
{
"SubjectId":"10f95aa3-fb95-41eb-b754-5f729a092e30",
"Username":"[email protected]",
"Password":"swissgeol_assets",
"Claims": [
{
"Type": "name",
"Value": "Admin User",
"ValueType": "string"
},
{
"Type": "family_name",
"Value": "User",
"ValueType": "string"
},
{
"Type": "given_name",
"Value": "Admin",
"ValueType": "string"
},
{
"Type": "email",
"Value": "[email protected]",
"ValueType": "string"
},
{
"Type": "email_verified",
"Value": "true",
"ValueType": "boolean"
},
{
"Type": "local_groups_claim",
"Value": "[\"boreholes_dev_group\"]",
"ValueType": "json"
}
]
},
{
"SubjectId":"sub_editor",
"Username":"editor",
"Password":"swissforages",
"Claims": [
{
"Type": "name",
"Value": "Editor User",
"ValueType": "string"
},
{
"Type": "family_name",
"Value": "User",
"ValueType": "string"
},
{
"Type": "given_name",
"Value": "Editor",
"ValueType": "string"
},
{
"Type": "email",
"Value": "[email protected]",
"ValueType": "string"
},
{
"Type": "email_verified",
"Value": "true",
"ValueType": "boolean"
},
{
"Type": "local_groups_claim",
"Value": "[\"boreholes_dev_group\"]",
"ValueType": "json"
}
]
}
]
9 changes: 8 additions & 1 deletion libs/auth/src/lib/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { ForModule } from '@rx-angular/template/for';
import { LetModule } from '@rx-angular/template/let';
import { PushModule } from '@rx-angular/template/push';
import { OAuthModule } from 'angular-oauth2-oidc';

import { AnchorComponent, ButtonComponent, icons } from '@asset-sg/client-shared';

Expand Down Expand Up @@ -46,9 +47,15 @@ import { SetPasswordPageComponent } from './components/set-password-page';
MatFormFieldModule,
MatInputModule,
MatProgressBarModule,

ButtonComponent,
AnchorComponent,
OAuthModule.forRoot({
resourceServer: {
sendAccessToken: true,
//allowedUrls:['http://localhost:3000'],
},
}
),
],
providers: [provideSvgIcons(icons)],
declarations: [ResetPasswordDialogComponent, SetPasswordDialogComponent],
Expand Down
21 changes: 21 additions & 0 deletions libs/auth/src/lib/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import * as RD from '@devexperts/remote-data-ts';
import { TranslateService } from '@ngx-translate/core';
import { OAuthService } from 'angular-oauth2-oidc';
import * as E from 'fp-ts/Either';
import { flow } from 'fp-ts/function';
import * as D from 'io-ts/Decoder';
Expand All @@ -24,13 +25,28 @@ export class AuthService {
private _httpClient = inject(HttpClient);
private _dcmnt = inject(DOCUMENT);
private _translateService = inject(TranslateService);
private _oauthService = inject(OAuthService);

private _getUserProfile() {
return this._httpClient
.get('/api/user')
.pipe(map(decode(User)), OE.catchErrorW(httpErrorResponseOrUnknownError));
}

init(): void {
this._oauthService.configure({
issuer: 'http://localhost:4011',
redirectUri: window.location.origin,
postLogoutRedirectUri: window.location.origin,
clientId: 'assets-client',
scope: 'openid profile local_groups_scope',
responseType: 'code',
showDebugInformation: true,
});
this._oauthService.loadDiscoveryDocumentAndLogin();
this._oauthService.setupAutomaticSilentRefresh();
}

getUserProfile(): ORD.ObservableRemoteData<ApiError, User> {
return this._getUserProfile().pipe(map(RD.fromEither), startWith(RD.pending));
}
Expand All @@ -55,7 +71,12 @@ export class AuthService {
);
}

logOut(): void {
this._oauthService.logOut();
}

logout(): ORD.ObservableRemoteData<ApiError, void> {
this._oauthService.logOut();
const accessToken = localStorage.getItem('accessToken');
return this._httpClient
.post(
Expand Down
4 changes: 1 addition & 3 deletions libs/profile/src/lib/components/profile/profile.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,9 @@ export class ProfileComponent extends RxState<ProfileComponentState> {
this._cd.detectChanges();
});

this.logoutClicked$.pipe(switchMap(() => this._authService.logout())).subscribe(rd => {
if (rdIsComplete(rd)) {
this.logoutClicked$.pipe(switchMap(async () => this._authService.logOut())).subscribe(rd => {
this._store.dispatch(appSharedStateActions.logout());
this._router.navigate(['/']);
}
});
}
}
Loading

0 comments on commit 5a31f4f

Please sign in to comment.