Skip to content

Commit 99ba6d8

Browse files
BacLucclaude
andcommitted
feat: replace external OAuth providers with navikt/mock-oauth2-server for local dev
In APP_ENV=dev (local Docker development), all four OAuth providers (Google, PBS MiData, CeviDB, JublaDB) are now backed by navikt/mock-oauth2-server instead of reaching out to real external services. This lets developers and e2e tests exercise the full OAuth login flow— including the browser redirect, user-selection screen and callback— without real credentials or network access to external identity providers. ## What is NOT changed - Helm charts (.helm/) are untouched: deployed environments (feature branches, staging, production) still use the real external OAuth providers. - Existing knpu_oauth2_client.yaml (prod config) is untouched. - GoogleAuthenticator and HitobitoAuthenticator are untouched and still used except for url and claim list overrides. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: BacLuc <lucius.bachmann@clubpage.ch>
1 parent d51e288 commit 99ba6d8

10 files changed

Lines changed: 452 additions & 16 deletions

File tree

api/.env

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,12 @@ JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
4949
JWT_PASSPHRASE=6521faf03b5be499f97cf0b40dc495f2
5050
###< lexik/jwt-authentication-bundle ###
5151

52-
###> league/oauth2-google ###
53-
OAUTH_GOOGLE_CLIENT_ID="889440431087-ueuhpadf2g7h5ucdke92mvfaf4l779m4.apps.googleusercontent.com"
54-
OAUTH_GOOGLE_CLIENT_SECRET="HNaD1FNO-a1qliacIrIfcGqO"
55-
###< league/oauth2-google ###
56-
57-
OAUTH_PBSMIDATA_CLIENT_ID="2a955efdaaac73f665b29ec182cd9a114db01675ced710a464d33d10f58be600"
58-
OAUTH_PBSMIDATA_CLIENT_SECRET="00a23e48bcb776d453b255428ffe810643db7155a9f3d743d7edf52eac400580"
59-
OAUTH_PBSMIDATA_BASE_URL="https://pbs.puzzle.ch"
60-
61-
OAUTH_CEVIDB_CLIENT_ID="raT1QFf6TOQzpn3yVH-My6YLrmsvOrfMhYypxzjPMWk"
62-
OAUTH_CEVIDB_CLIENT_SECRET="fTxMrzjBn3gPGg3eB0bNMmjRqg4ccs3_su7CaTXtljE"
63-
OAUTH_CEVIDB_BASE_URL="https://cevi.puzzle.ch"
64-
65-
OAUTH_JUBLADB_CLIENT_ID="WrKABq7GwmC6h1F0W73OGX_fOTHWWXnKXfrPMHOdQWY"
66-
OAUTH_JUBLADB_CLIENT_SECRET="oQ164RDMIAocL6PhmCoeT1Ymcg-7WcOJZdxCnIph5gM"
67-
OAUTH_JUBLADB_BASE_URL="https://jubla.puzzle.ch"
52+
# Mock OAuth2 server (used in APP_ENV=dev via config/packages/dev/knpu_oauth2_client.yaml)
53+
# These defaults assume the API is running inside Docker (docker-compose.override.yml
54+
# sets these env vars to the correct Docker-internal addresses automatically).
55+
MOCK_OAUTH_INTERNAL_BASE_URL="http://mock-oauth2:8080"
56+
MOCK_OAUTH_EXTERNAL_BASE_URL="http://localhost:3000/mock-auth"
57+
MOCK_HITOBITO_INTERNAL_BASE_URL='http://reverse-proxy:3005'
6858

6959
###> sentry/sentry-symfony ###
7060
SENTRY_API_DSN=""

api/config/packages/knpu_oauth2_client.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,31 @@ knpu_oauth2_client:
7070
client_secret: '%env(OAUTH_JUBLADB_CLIENT_SECRET)%'
7171
redirect_route: connect_jubladb_check
7272
redirect_params: {}
73+
74+
when@dev:
75+
knpu_oauth2_client:
76+
clients:
77+
google:
78+
provider_class: App\OAuth\MockGoogle
79+
provider_options:
80+
baseUrl: '%env(MOCK_OAUTH_INTERNAL_BASE_URL)%/google'
81+
externalBaseUrl: '%env(MOCK_OAUTH_EXTERNAL_BASE_URL)%/google'
82+
pbsmidata:
83+
provider_class: App\OAuth\MockHitobito
84+
provider_options:
85+
baseUrl: '%env(MOCK_HITOBITO_INTERNAL_BASE_URL)%/pbsmidata'
86+
externalBaseUrl: '%env(MOCK_OAUTH_EXTERNAL_BASE_URL)%/pbsmidata'
87+
# todo: check if really needed
88+
realBaseUrl: '%env(OAUTH_PBSMIDATA_BASE_URL)%'
89+
cevidb:
90+
provider_class: App\OAuth\MockHitobito
91+
provider_options:
92+
baseUrl: '%env(MOCK_HITOBITO_INTERNAL_BASE_URL)%/cevidb'
93+
externalBaseUrl: '%env(MOCK_OAUTH_EXTERNAL_BASE_URL)%/cevidb'
94+
realBaseUrl: '%env(OAUTH_CEVIDB_BASE_URL)%'
95+
jubladb:
96+
provider_class: App\OAuth\MockHitobito
97+
provider_options:
98+
baseUrl: '%env(MOCK_HITOBITO_INTERNAL_BASE_URL)%/jubladb'
99+
externalBaseUrl: '%env(MOCK_OAUTH_EXTERNAL_BASE_URL)%/jubladb'
100+
realBaseUrl: '%env(OAUTH_JUBLADB_BASE_URL)%'

api/src/OAuth/MockGoogle.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\OAuth;
6+
7+
use League\OAuth2\Client\Provider\Google;
8+
use League\OAuth2\Client\Token\AccessToken;
9+
10+
/**
11+
* Google OAuth2 provider configured to use navikt/mock-oauth2-server for local development.
12+
*
13+
* Extends the real Google provider so the production code path (GoogleUser, assertMatchingDomain,
14+
* scope handling) is exercised unchanged. Only the three hard-coded Google URLs are replaced with
15+
* configurable ones pointing at the mock server.
16+
*/
17+
class MockGoogle extends Google {
18+
/** Internal Docker network URL (API → mock-oauth2 container), includes issuer path. */
19+
protected string $baseUrl;
20+
21+
/** External URL reachable by the browser (proxied through nginx at /mock-auth). */
22+
protected string $externalBaseUrl;
23+
24+
public function getBaseAuthorizationUrl(): string {
25+
return $this->externalBaseUrl.'/authorize';
26+
}
27+
28+
public function getBaseAccessTokenUrl(array $params): string {
29+
return $this->baseUrl.'/token';
30+
}
31+
32+
public function getResourceOwnerDetailsUrl(AccessToken $token): string {
33+
return $this->baseUrl.'/userinfo';
34+
}
35+
}

api/src/OAuth/MockHitobito.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\OAuth;
6+
7+
/**
8+
* Hitobito OAuth2 provider configured to use navikt/mock-oauth2-server for local development.
9+
*
10+
* Extends the real Hitobito provider unchanged so the full production code path
11+
* (HitobitoUser parsing, X-Scope header, /oauth/profile endpoint) is exercised.
12+
*
13+
* The only override is getBaseAuthorizationUrl(), which must use the browser-visible
14+
* externalBaseUrl so the user is redirected to the nginx-proxied mock server URL.
15+
* All internal API calls (token exchange, userinfo) go through a dedicated nginx
16+
* server block (port 3005) that translates Hitobito-style paths to the mock server's
17+
* OIDC paths:
18+
*
19+
* /{issuer}/oauth/token → /{issuer}/token
20+
* /{issuer}/oauth/profile → /{issuer}/userinfo
21+
*
22+
* The X-Scope header added by the parent's fetchResourceOwnerDetails() is silently
23+
* ignored by navikt/mock-oauth2-server, so no override is needed.
24+
*
25+
* See reverse-proxy-nginx.conf (port 3005 server block).
26+
*/
27+
class MockHitobito extends Hitobito {
28+
/** External URL reachable by the browser (proxied through nginx at /mock-auth). */
29+
protected string $externalBaseUrl;
30+
31+
/** baseUrl is inherited from Hitobito and points to the nginx hitobito adapter (port 3005). */
32+
public function getBaseAuthorizationUrl(): string {
33+
// The browser-visible authorize URL uses the Hitobito /oauth/authorize path;
34+
// nginx at port 3000 translates /mock-auth/{issuer}/oauth/authorize → /{issuer}/authorize.
35+
return $this->externalBaseUrl.'/oauth/authorize';
36+
}
37+
38+
#[\Override]
39+
protected function getDefaultScopes(): array {
40+
// navikt/mock-oauth2-server requires openid in the scope (OIDC mandate).
41+
return ['openid', 'name'];
42+
}
43+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"interactiveLogin": true,
3+
"loginPagePath": "/app/login.html"
4+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Mock OAuth2 Sign-in</title>
6+
<style>
7+
body {
8+
font-family: sans-serif;
9+
max-width: 480px;
10+
margin: 60px auto;
11+
padding: 0 16px;
12+
}
13+
h2 {
14+
margin-bottom: 4px;
15+
}
16+
p.subtitle {
17+
color: #666;
18+
font-size: 0.9em;
19+
margin-top: 0;
20+
margin-bottom: 24px;
21+
}
22+
.user-btn {
23+
display: block;
24+
width: 100%;
25+
margin-bottom: 10px;
26+
padding: 10px 16px;
27+
font-size: 1em;
28+
text-align: left;
29+
cursor: pointer;
30+
background: #f5f5f5;
31+
border: 1px solid #ccc;
32+
border-radius: 4px;
33+
}
34+
.user-btn:hover {
35+
background: #e8e8e8;
36+
}
37+
.user-email {
38+
font-weight: bold;
39+
}
40+
.user-name {
41+
color: #555;
42+
font-size: 0.9em;
43+
}
44+
hr {
45+
margin: 24px 0;
46+
}
47+
.custom-row {
48+
display: flex;
49+
gap: 8px;
50+
}
51+
.custom-row input {
52+
flex: 1;
53+
padding: 8px;
54+
font-size: 1em;
55+
border: 1px solid #ccc;
56+
border-radius: 4px;
57+
}
58+
.custom-row button {
59+
padding: 8px 16px;
60+
font-size: 1em;
61+
cursor: pointer;
62+
border-radius: 4px;
63+
border: 1px solid #aaa;
64+
}
65+
textarea[name='claims'] {
66+
min-width: 100%;
67+
}
68+
</style>
69+
</head>
70+
<body>
71+
<h2>Mock OAuth2 Sign-in</h2>
72+
<p class="subtitle">Local development only – not present in production.</p>
73+
74+
<!-- ── Pre-configured test users ─────────────────────────────────────── -->
75+
<!--
76+
Claims are universal: include both OIDC fields (given_name/family_name for
77+
GoogleUser) and Hitobito fields (first_name/last_name/id for HitobitoUser).
78+
The username becomes the token sub when sub is not in the claims JSON.
79+
-->
80+
81+
<form method="post">
82+
<input type="hidden" name="username" value="test@example.com" />
83+
<input
84+
type="hidden"
85+
name="claims"
86+
value='{"email":"test@example.com","name":"Test User","given_name":"Test","family_name":"User","id":1001,"first_name":"Test","last_name":"User","nickname":"test"}'
87+
/>
88+
<button type="submit" class="user-btn">
89+
<span class="user-email">test@example.com</span><br />
90+
<span class="user-name">Test User</span>
91+
</button>
92+
</form>
93+
94+
<form method="post">
95+
<input type="hidden" name="username" value="test2@example.com" />
96+
<input
97+
type="hidden"
98+
name="claims"
99+
value='{"email":"test2@example.com","name":"Test User 2","given_name":"Test","family_name":"User2","id":1002,"first_name":"Test","last_name":"User2","nickname":"test2"}'
100+
/>
101+
<button type="submit" class="user-btn">
102+
<span class="user-email">test2@example.com</span><br />
103+
<span class="user-name">Test User 2</span>
104+
</button>
105+
</form>
106+
107+
<form method="post">
108+
<input type="hidden" name="username" value="admin@example.com" />
109+
<input
110+
type="hidden"
111+
name="claims"
112+
value='{"email":"admin@example.com","name":"Admin User","given_name":"Admin","family_name":"User","id":1003,"first_name":"Admin","last_name":"User","nickname":"admin"}'
113+
/>
114+
<button type="submit" class="user-btn">
115+
<span class="user-email">admin@example.com</span><br />
116+
<span class="user-name">Admin User</span>
117+
</button>
118+
</form>
119+
120+
<hr />
121+
<form method="post">
122+
<div class="custom-row">
123+
<input type="hidden" name="username" placeholder="Enter email (or leave empty)" />
124+
<!--suppress HtmlFormInputWithoutLabel -->
125+
<textarea name="claims" rows="10">
126+
{
127+
"email":"my@email.com",
128+
"given_name":"Custom",
129+
"family_name":"User",
130+
"first_name":"Custom",
131+
"last_name":"User",
132+
"nickname": "nickname",
133+
"id":9999
134+
}
135+
</textarea
136+
>
137+
</div>
138+
<div class="custom-row">
139+
<button type="submit">Sign in</button>
140+
</div>
141+
</form>
142+
</body>
143+
</html>

docker-compose.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ services:
6060
depends_on:
6161
- database
6262
- docker-host
63+
- mock-oauth2
6364
volumes:
6465
- ./api/config/jwt:/app/config/jwt:rw,delegated
6566
ports:
@@ -220,6 +221,15 @@ services:
220221
network_mode: host
221222
working_dir: /e2e
222223

224+
mock-oauth2:
225+
image: ghcr.io/navikt/mock-oauth2-server:4.0.0
226+
volumes:
227+
- ./dev-setup/mock-oauth/mock-oauth2-config.json:/app/config.json:ro
228+
- ./dev-setup/mock-oauth/mock-oauth2-login.html:/app/login.html:ro
229+
environment:
230+
JSON_CONFIG_PATH: /app/config.json
231+
SERVER_PORT: 8080
232+
223233
reverse-proxy:
224234
image: nginx:1.31
225235
container_name: 'ecamp3-reverse-proxy'
@@ -229,6 +239,7 @@ services:
229239
- frontend
230240
- api
231241
- frontend-old
242+
- mock-oauth2
232243
ports:
233244
- target: 3000
234245
published: 3000

0 commit comments

Comments
 (0)