Skip to content

Commit 2d09e05

Browse files
author
morteza.khoramdel
committed
add auth
1 parent af672cb commit 2d09e05

12 files changed

Lines changed: 244 additions & 23 deletions

File tree

Caddyfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,8 @@
1717
X-Permitted-Cross-Domain-Policies "none"
1818
Referrer-Policy "no-referrer"
1919
}
20+
21+
header /assets/auth-config.json {
22+
Cache-Control "no-store"
23+
}
2024
}

Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,11 @@ FROM caddy:2.10.2
2525
ENV PORT=8080
2626

2727
COPY Caddyfile /etc/caddy/Caddyfile
28+
COPY docker/docker-entrypoint.sh /usr/local/bin/dsomm-entrypoint
2829
COPY --from=build ["/usr/src/app/dist/dsomm/", "/srv"]
2930
COPY --from=yaml ["/var/www/html/generated/model.yaml", "/srv/assets/YAML/default/model.yaml"]
31+
32+
RUN chmod +x /usr/local/bin/dsomm-entrypoint && chown -R caddy:caddy /srv/assets
33+
34+
ENTRYPOINT ["/usr/local/bin/dsomm-entrypoint"]
35+
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

README.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ This page uses the Browser's localStorage to store the state of the circular hea
2929

3030
# Static Demo Authentication
3131

32-
This Angular frontend includes simple static-user authentication for demo and internal
32+
This Angular frontend includes simple runtime-configured authentication for demo and internal
3333
deployments. All users have the same permissions.
3434

35-
Default credentials are defined in `src/app/services/auth.service.ts`:
35+
For local development, default demo credentials are defined in `src/assets/auth-config.json`:
3636

3737
| Username | Password |
3838
| --- | --- |
@@ -41,11 +41,38 @@ Default credentials are defined in `src/app/services/auth.service.ts`:
4141
| `developer` | `dsomm-dev` |
4242
| `viewer` | `dsomm-view` |
4343

44+
For Docker deployments, define users at container startup instead of rebuilding the image:
45+
46+
```yaml
47+
services:
48+
dsomm:
49+
image: wurstbrot/dsomm:latest
50+
ports:
51+
- "8080:8080"
52+
environment:
53+
DSOMM_AUTH_USERS: >-
54+
[
55+
{"username":"admin","password":"change-me"},
56+
{"username":"auditor","password":"audit-me"}
57+
]
58+
```
59+
60+
The container entrypoint writes `DSOMM_AUTH_USERS` to `/srv/assets/auth-config.json`. You can also
61+
mount your own config file at `/srv/assets/auth-config.json` with this shape:
62+
63+
```json
64+
{
65+
"users": [
66+
{ "username": "admin", "password": "change-me" }
67+
]
68+
}
69+
```
70+
4471
Sign in at `/login`. The app stores the current user in the browser's `sessionStorage`, so the
4572
login lasts only for the current browser session.
4673

4774
Security warning: this is frontend-only authentication. It is not secure for production because
48-
static credentials are shipped in the browser bundle and can be inspected by users. Use a backend
75+
the browser must receive the auth config and credentials can be inspected by users. Use a backend
4976
identity provider or server-side access control for production deployments.
5077

5178
# Changes

docker-compose.example.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
dsomm:
3+
build: .
4+
ports:
5+
- "8080:8080"
6+
environment:
7+
DSOMM_AUTH_USERS: >-
8+
[
9+
{"username":"admin","password":"change-me"},
10+
{"username":"auditor","password":"audit-me"},
11+
{"username":"developer","password":"dev-me"}
12+
]

docker/docker-entrypoint.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
AUTH_CONFIG_PATH="${DSOMM_AUTH_CONFIG_PATH:-/srv/assets/auth-config.json}"
5+
6+
if [ -n "${DSOMM_AUTH_USERS:-}" ]; then
7+
mkdir -p "$(dirname "$AUTH_CONFIG_PATH")"
8+
{
9+
printf '{\n "users": '
10+
printf '%s' "$DSOMM_AUTH_USERS"
11+
printf '\n}\n'
12+
} > "$AUTH_CONFIG_PATH"
13+
elif [ ! -f "$AUTH_CONFIG_PATH" ]; then
14+
mkdir -p "$(dirname "$AUTH_CONFIG_PATH")"
15+
printf '{\n "users": []\n}\n' > "$AUTH_CONFIG_PATH"
16+
fi
17+
18+
exec "$@"

src/app/app.module.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NgModule } from '@angular/core';
1+
import { APP_INITIALIZER, NgModule } from '@angular/core';
22
import { BrowserModule } from '@angular/platform-browser';
33
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
44
import { MatToolbarModule } from '@angular/material/toolbar';
@@ -41,6 +41,11 @@ import { AddEvidenceModalComponent } from './component/add-evidence-modal/add-ev
4141
import { EvidencePanelComponent } from './component/evidence-panel/evidence-panel.component';
4242
import { ViewEvidenceModalComponent } from './component/view-evidence-modal/view-evidence-modal.component';
4343
import { LoginComponent } from './pages/login/login.component';
44+
import { AuthService } from './services/auth.service';
45+
46+
export function initializeAuth(authService: AuthService): () => Promise<void> {
47+
return () => authService.loadConfig();
48+
}
4449

4550
@NgModule({
4651
declarations: [
@@ -91,6 +96,7 @@ import { LoginComponent } from './pages/login/login.component';
9196
providers: [
9297
LoaderService,
9398
ModalMessageComponent,
99+
{ provide: APP_INITIALIZER, useFactory: initializeAuth, deps: [AuthService], multi: true },
94100
{ provide: MAT_DIALOG_DATA, useValue: {} },
95101
{ provide: MatDialogRef, useValue: { close: (dialogResult: any) => {} } },
96102
],

src/app/guards/auth.guard.spec.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,38 @@
11
import { TestBed } from '@angular/core/testing';
2+
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
23
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
34
import { RouterTestingModule } from '@angular/router/testing';
45
import { AuthGuard } from './auth.guard';
56
import { AuthService } from '../services/auth.service';
67

8+
const authUsers = [
9+
{ username: 'auditor', password: 'dsomm-audit' },
10+
{ username: 'developer', password: 'dsomm-dev' },
11+
];
12+
713
describe('AuthGuard', () => {
814
let guard: AuthGuard;
915
let authService: AuthService;
1016
let router: Router;
17+
let httpMock: HttpTestingController;
1118
const sessionUserKey = 'dsomm.auth.currentUser';
1219

13-
beforeEach(() => {
20+
beforeEach(async () => {
1421
TestBed.configureTestingModule({
15-
imports: [RouterTestingModule.withRoutes([])],
22+
imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])],
1623
});
1724

1825
guard = TestBed.inject(AuthGuard);
1926
authService = TestBed.inject(AuthService);
2027
router = TestBed.inject(Router);
28+
httpMock = TestBed.inject(HttpTestingController);
2129
sessionStorage.removeItem(sessionUserKey);
30+
31+
await loadAuthConfig();
2232
});
2333

2434
afterEach(() => {
35+
httpMock.verify();
2536
sessionStorage.removeItem(sessionUserKey);
2637
});
2738

@@ -67,3 +78,12 @@ function routeSnapshot(): ActivatedRouteSnapshot {
6778
function routerState(url: string): RouterStateSnapshot {
6879
return { url } as RouterStateSnapshot;
6980
}
81+
82+
async function loadAuthConfig(): Promise<void> {
83+
const authService = TestBed.inject(AuthService);
84+
const httpMock = TestBed.inject(HttpTestingController);
85+
const configLoaded = authService.loadConfig();
86+
const request = httpMock.expectOne('assets/auth-config.json');
87+
request.flush({ users: authUsers });
88+
await configLoaded;
89+
}

src/app/pages/login/login.component.spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { NO_ERRORS_SCHEMA } from '@angular/core';
2+
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
23
import { ComponentFixture, TestBed } from '@angular/core/testing';
34
import { ReactiveFormsModule } from '@angular/forms';
45
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
@@ -11,8 +12,14 @@ describe('LoginComponent', () => {
1112
let fixture: ComponentFixture<LoginComponent>;
1213
let authService: AuthService;
1314
let router: Router;
15+
let httpMock: HttpTestingController;
1416
let routeStub: { snapshot: { queryParamMap: ReturnType<typeof convertToParamMap> } };
1517
const sessionUserKey = 'dsomm.auth.currentUser';
18+
const authUsers = [
19+
{ username: 'admin', password: 'dsomm-admin' },
20+
{ username: 'auditor', password: 'dsomm-audit' },
21+
{ username: 'viewer', password: 'dsomm-view' },
22+
];
1623

1724
beforeEach(async () => {
1825
routeStub = {
@@ -23,17 +30,21 @@ describe('LoginComponent', () => {
2330

2431
await TestBed.configureTestingModule({
2532
declarations: [LoginComponent],
26-
imports: [ReactiveFormsModule, RouterTestingModule.withRoutes([])],
33+
imports: [HttpClientTestingModule, ReactiveFormsModule, RouterTestingModule.withRoutes([])],
2734
providers: [{ provide: ActivatedRoute, useValue: routeStub }],
2835
schemas: [NO_ERRORS_SCHEMA],
2936
}).compileComponents();
3037

3138
authService = TestBed.inject(AuthService);
3239
router = TestBed.inject(Router);
40+
httpMock = TestBed.inject(HttpTestingController);
3341
sessionStorage.removeItem(sessionUserKey);
42+
43+
await loadAuthConfig();
3444
});
3545

3646
afterEach(() => {
47+
httpMock.verify();
3748
sessionStorage.removeItem(sessionUserKey);
3849
});
3950

@@ -96,4 +107,11 @@ describe('LoginComponent', () => {
96107

97108
expect(navigateSpy).toHaveBeenCalledOnceWith('/');
98109
});
110+
111+
async function loadAuthConfig(): Promise<void> {
112+
const configLoaded = authService.loadConfig();
113+
const request = httpMock.expectOne('assets/auth-config.json');
114+
request.flush({ users: authUsers });
115+
await configLoaded;
116+
}
99117
});
Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,77 @@
11
import { TestBed } from '@angular/core/testing';
2+
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
23
import { AuthService } from './auth.service';
34

45
describe('AuthService', () => {
56
let service: AuthService;
7+
let httpMock: HttpTestingController;
68
const sessionUserKey = 'dsomm.auth.currentUser';
9+
const authUsers = [
10+
{ username: 'admin', password: 'dsomm-admin' },
11+
{ username: 'viewer', password: 'dsomm-view' },
12+
];
713

814
beforeEach(() => {
9-
TestBed.configureTestingModule({});
15+
TestBed.configureTestingModule({
16+
imports: [HttpClientTestingModule],
17+
});
1018
service = TestBed.inject(AuthService);
19+
httpMock = TestBed.inject(HttpTestingController);
1120
sessionStorage.removeItem(sessionUserKey);
1221
});
1322

1423
afterEach(() => {
24+
httpMock.verify();
1525
sessionStorage.removeItem(sessionUserKey);
1626
});
1727

18-
it('logs in a static user with valid credentials', () => {
28+
it('logs in a configured user with valid credentials', async () => {
29+
await loadAuthConfig();
30+
1931
const loggedIn = service.login('admin', 'dsomm-admin');
2032

2133
expect(loggedIn).toBeTrue();
2234
expect(service.isAuthenticated()).toBeTrue();
2335
expect(service.getCurrentUser()).toBe('admin');
2436
});
2537

26-
it('rejects invalid credentials', () => {
38+
it('rejects invalid credentials', async () => {
39+
await loadAuthConfig();
40+
2741
const loggedIn = service.login('admin', 'wrong-password');
2842

2943
expect(loggedIn).toBeFalse();
3044
expect(service.isAuthenticated()).toBeFalse();
3145
expect(service.getCurrentUser()).toBeNull();
3246
});
3347

34-
it('clears the authenticated user on logout', () => {
48+
it('clears the authenticated user on logout', async () => {
49+
await loadAuthConfig();
50+
3551
service.login('viewer', 'dsomm-view');
3652

3753
service.logout();
3854

3955
expect(service.isAuthenticated()).toBeFalse();
4056
expect(service.getCurrentUser()).toBeNull();
4157
});
58+
59+
it('rejects login when the auth config cannot be loaded', async () => {
60+
const configLoaded = service.loadConfig();
61+
const request = httpMock.expectOne('assets/auth-config.json');
62+
request.flush('Not found', { status: 404, statusText: 'Not Found' });
63+
64+
await configLoaded;
65+
66+
expect(service.login('admin', 'dsomm-admin')).toBeFalse();
67+
expect(service.isAuthenticated()).toBeFalse();
68+
});
69+
70+
async function loadAuthConfig(): Promise<void> {
71+
const configLoaded = service.loadConfig();
72+
const request = httpMock.expectOne('assets/auth-config.json');
73+
expect(request.request.method).toBe('GET');
74+
request.flush({ users: authUsers });
75+
await configLoaded;
76+
}
4277
});

src/app/services/auth.service.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
11
import { Injectable } from '@angular/core';
2+
import { HttpClient } from '@angular/common/http';
3+
import { firstValueFrom } from 'rxjs';
24

35
interface StaticUser {
46
username: string;
57
password: string;
68
}
79

10+
interface AuthConfig {
11+
users?: StaticUser[];
12+
}
13+
814
@Injectable({ providedIn: 'root' })
915
export class AuthService {
1016
private readonly SESSION_USER_KEY = 'dsomm.auth.currentUser';
17+
private readonly configUrl = 'assets/auth-config.json';
18+
private users: StaticUser[] = [];
1119

1220
/*
13-
* Demo/internal-only static users.
14-
* This is not secure for production: credentials in frontend code are visible
15-
* to anyone who can load the application bundle.
21+
* Demo/internal-only frontend auth.
22+
* This is not secure for production: browser-delivered auth config is visible
23+
* to anyone who can load the application.
1624
*/
17-
private readonly users: StaticUser[] = [
18-
{ username: 'admin', password: 'dsomm-admin' },
19-
{ username: 'auditor', password: 'dsomm-audit' },
20-
{ username: 'developer', password: 'dsomm-dev' },
21-
{ username: 'viewer', password: 'dsomm-view' },
22-
];
25+
constructor(private http: HttpClient) {}
26+
27+
async loadConfig(): Promise<void> {
28+
try {
29+
const config = await firstValueFrom(this.http.get<AuthConfig>(this.configUrl));
30+
this.users = this.normalizeUsers(config);
31+
} catch (_err) {
32+
this.users = [];
33+
}
34+
}
2335

2436
login(username: string, password: string): boolean {
2537
const matchedUser = this.users.find(
@@ -45,4 +57,20 @@ export class AuthService {
4557
getCurrentUser(): string | null {
4658
return sessionStorage.getItem(this.SESSION_USER_KEY);
4759
}
60+
61+
private normalizeUsers(config: AuthConfig | null): StaticUser[] {
62+
if (!config || !Array.isArray(config.users)) {
63+
return [];
64+
}
65+
66+
const users: StaticUser[] = config.users;
67+
68+
return users.filter(
69+
(user): user is StaticUser =>
70+
typeof user?.username === 'string' &&
71+
user.username.length > 0 &&
72+
typeof user?.password === 'string' &&
73+
user.password.length > 0
74+
);
75+
}
4876
}

0 commit comments

Comments
 (0)