Skip to content

Commit 88a191f

Browse files
authored
merge: Merge pull request #113 from arunaengine/feat/iam4nfdi
IAM4NFDI & OIDC support improvements
2 parents 7b387bf + a775830 commit 88a191f

File tree

6 files changed

+171
-96
lines changed

6 files changed

+171
-96
lines changed

components/custom-ui/dialog/LoginDialog.vue

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ const local = computed(() => window.location.hostname.includes('localhost'))
2929
<Button v-if='local'
3030
variant="outline"
3131
class="px-4 border-aruna-text/50 text-lg hover:bg-aruna-fg">
32-
<a href="/auth/login?provider=local" class="flex">
32+
<a href="/auth/login?provider=local" class="flex items-center">
3333
<img src="/imgs/keycloak.webp"
3434
alt="Local Test Login"
35-
class="h-6 mr-2"/>
36-
Local Test Login
35+
class="flex h-6 mr-3"/>
36+
<p class="flex">Local Test Login</p>
3737
</a>
3838
</Button>
3939

4040
<Button variant="outline"
4141
class="px-4 border-aruna-text/50 text-lg hover:bg-aruna-fg">
42-
<a href="/auth/login?provider=lifescience" class="flex">
42+
<a href="/auth/login?provider=lifescience" class="flex items-center">
4343
<img src="/imgs/ls-ri.webp"
4444
alt="LifeScience Login"
4545
class="h-6 mr-2"/>
@@ -48,14 +48,24 @@ const local = computed(() => window.location.hostname.includes('localhost'))
4848
</Button>
4949

5050
<Button variant="outline"
51-
class="px-4 border-aruna-text/50 text-lg hover:bg-aruna-fg">
52-
<a href="/auth/login?provider=gfbio" class="flex">
51+
class="px-4 border-aruna-text/50 text-lg hover:bg-aruna-fg">
52+
<a href="/auth/login?provider=gfbio" class="flex items-center">
5353
<img src="/imgs/gfbio.webp"
5454
alt="GFBio Logo"
5555
class="h-6 mr-2"/>
5656
GFBio SSO
5757
</a>
5858
</Button>
59+
60+
<Button variant="outline"
61+
class="px-4 border-aruna-text/50 text-lg hover:bg-aruna-fg">
62+
<a href="/auth/login?provider=iam4nfdi" class="flex items-center">
63+
<img src="/imgs/iam4nfdi.webp"
64+
alt="IAM4NFDI Logo"
65+
class="h-6 mr-2"/>
66+
NFDI AAI
67+
</a>
68+
</Button>
5969
</DialogContent>
6070
</Dialog>
6171
</template>

nuxt.config.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,37 +39,40 @@ export default defineNuxtConfig({
3939
},
4040
provider: {
4141
local: {
42+
wellKnownUrl: "http://localhost:1998/realms/test/.well-known/openid-configuration",
4243
clientId: "test",
4344
clientSecret: "QgBl9I2CD3eVhL7LFvkHrYUK7oKL3LE2",
44-
issuer: "http://localhost:1998/realms/test",
4545
redirectUrl: "http://localhost:3000/callback",
46-
authUrl: 'http://localhost:1998/realms/test/protocol/openid-connect/auth',
47-
tokenUrl: 'http://localhost:1998/realms/test/protocol/openid-connect/token',
48-
revokeUrl: 'http://localhost:1998/realms/test/protocol/openid-connect/revoke',
4946
scope: ["openid"],
50-
code_challenge: false
47+
code_challenge: false,
48+
post_auth: false
5149
},
5250
gfbio: {
51+
wellKnownUrl: "",
5352
clientId: '',
5453
clientSecret: '',
55-
issuer: '',
5654
redirectUrl: '',
57-
authUrl: '',
58-
tokenUrl: '',
59-
revokeUrl: '',
6055
scope: [''],
61-
code_challenge: false
56+
code_challenge: false,
57+
post_auth: false
6258
},
6359
lifescience: {
60+
wellKnownUrl: "",
6461
clientId: '',
6562
clientSecret: '',
66-
issuer: '',
6763
redirectUrl: '',
68-
authUrl: '',
69-
tokenUrl: '',
70-
revokeUrl: '',
7164
scope: [''],
72-
code_challenge: false
65+
code_challenge: false,
66+
post_auth: false
67+
},
68+
iam4nfdi: {
69+
wellKnownUrl: "",
70+
clientId: '',
71+
clientSecret: '',
72+
redirectUrl: '',
73+
scope: [''],
74+
code_challenge: false,
75+
post_auth: false
7376
}
7477
},
7578
markdownCss: {

public/imgs/iam4nfdi.webp

2.29 KB
Loading

server/routes/auth/login.get.ts

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {withQuery} from 'ufo'
22
import crypto from 'crypto'
3+
import {AuthQuery, IdpMetadata, errorToPOJO, fetchCachedOidcMetadata} from "~/server/utils/oidc";
34

45
export default defineEventHandler(async event => {
56
const provider = getQuery(event)['provider']
@@ -20,50 +21,48 @@ export default defineEventHandler(async event => {
2021
// Delete older cookie with login meta info
2122
deleteCookie(event, 'login_meta')
2223

24+
// Fetch idp metadata from well-known url
25+
const response = await fetchCachedOidcMetadata(config.wellKnownUrl)
26+
if (Array.isArray(response)) {
27+
return createError({
28+
statusCode: response[0],
29+
message: `Login failed: '${response[1]}'. Please try again later or contact the website administrator`,
30+
});
31+
}
32+
console.log(`[Login Server] Idp meta fetch ${response}`)
33+
34+
// Start authentication flow
35+
let auth_query: AuthQuery = {
36+
client_id: config.clientId,
37+
redirect_uri: config.redirectUrl,
38+
scope: config.scope.join(' '),
39+
response_type: 'code',
40+
}
41+
42+
let code_verifier
2343
if (config?.code_challenge) {
2444
//const state = crypto.randomUUID() // Optional
25-
const code_verifier = crypto.randomUUID()
26-
const code_challenge = crypto.createHash('sha256').update(code_verifier).digest().toString('base64')
45+
code_verifier = crypto.randomUUID()
46+
const code_challenge = crypto.createHash('sha256').update(code_verifier).digest().toString('base64url')
2747
.replace(/\+/g, '-')
2848
.replace(/\//g, '_')
2949
.replace(/=+$/, '')
3050

31-
// Cache login meta information in cookie for callback
32-
setCookie(event, 'login_meta', JSON.stringify( {
33-
provider: provider,
34-
code_verifier: code_verifier,
35-
add_idp: add_idp ? add_idp : false
36-
}))
51+
// Add PKCE relevant fields
52+
auth_query['code_challenge_method'] = 'S256'
53+
auth_query['code_challenge'] = code_challenge
54+
}
3755

38-
// Redirect to idp authorization
39-
return sendRedirect(
40-
event,
41-
withQuery(config.authUrl, {
42-
response_type: 'code',
43-
client_id: config.clientId,
44-
redirect_uri: config.redirectUrl,
45-
scope: config.scope.join(' '),
46-
code_challenge_method: 'S256',
47-
code_challenge: code_challenge
48-
})
49-
)
50-
} else {
51-
// Cache login meta information in cookie for callback
52-
setCookie(event, 'login_meta', JSON.stringify( {
53-
provider: provider,
54-
code_verifier: undefined,
55-
add_idp: add_idp ? add_idp : false
56-
}))
56+
// Cache login meta information in cookie for callback
57+
setCookie(event, 'login_meta', JSON.stringify({
58+
provider: provider,
59+
code_verifier: code_verifier,
60+
add_idp: add_idp ? add_idp : false
61+
}))
5762

58-
// Redirect to idp authorization without code challenge
59-
return sendRedirect(
60-
event,
61-
withQuery(config.authUrl, {
62-
client_id: config.clientId,
63-
redirect_uri: config.redirectUrl,
64-
scope: config.scope.join(' '),
65-
response_type: 'code',
66-
})
67-
)
68-
}
63+
// Redirect to idp authorization endpoint
64+
return sendRedirect(
65+
event,
66+
withQuery(response.authorization_endpoint, auth_query)
67+
)
6968
})

server/routes/callback.get.ts

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
import type {v2AddOidcProviderResponse} from "~/composables/aruna_api_json";
2+
import {errorToPOJO, fetchCachedOidcMetadata} from "~/server/utils/oidc";
3+
4+
type AuthCodeRequest = {
5+
grant_type: 'authorization_code',
6+
redirect_uri: string,
7+
code: string,
8+
code_verifier?: string, // Only with PKCE
9+
client_id?: string, // Only with client_secret_post (config.post_auth: true)
10+
client_secret?: string, // Only with client_secret_post (config.post_auth: true)
11+
}
12+
13+
type AuthCodeResponse = {
14+
access_token: string,
15+
refresh_token: string,
16+
expires_in: number | undefined,
17+
refresh_expires_in: number | undefined,
18+
}
219

320
export default defineEventHandler(async event => {
421
const query = getQuery(event)
@@ -16,7 +33,7 @@ export default defineEventHandler(async event => {
1633
deleteCookie(event, 'login_meta')
1734

1835
if (!login_meta)
19-
throw new Error('Missing login meta information')
36+
throw new Error('Missing login meta information')
2037
const {provider, code_verifier, add_idp} = JSON.parse(login_meta)
2138

2239
// Fetch provider specific config
@@ -26,43 +43,49 @@ export default defineEventHandler(async event => {
2643
throw new Error(`Unknown/Unsupported identity provider: ${provider}`)
2744
}
2845

29-
// Fetch access/refresh tokens from provider
30-
let tokens: any = undefined
46+
const response = await fetchCachedOidcMetadata(config.wellKnownUrl)
47+
if (Array.isArray(response)) {
48+
return createError({
49+
statusCode: response[0],
50+
message: `Failed to fetch openid configuration of identity provider: '${response[1]}'.
51+
Please try again later or contact the website administrator`,
52+
});
53+
}
54+
55+
// Build request and fetch access/refresh token from provider
56+
let request: AuthCodeRequest = {
57+
grant_type: 'authorization_code',
58+
redirect_uri: config.redirectUrl,
59+
code: code as string,
60+
}
3161
if (code_verifier) {
32-
// Fetch access and refresh token
33-
tokens = await $fetch(config.tokenUrl, {
34-
method: 'POST',
35-
headers: {
36-
'Content-Type': 'application/x-www-form-urlencoded',
37-
},
38-
body: new URLSearchParams({
39-
client_id: config.clientId,
40-
client_secret: config.clientSecret,
41-
grant_type: 'authorization_code',
42-
redirect_uri: config.redirectUrl,
43-
code: code as string,
44-
code_verifier: code_verifier,
45-
}).toString(),
46-
}).catch((error) => {
47-
return {error}
48-
})
49-
} else {
50-
// Fetch access and refresh token
51-
tokens = await $fetch(config.tokenUrl, {
52-
method: 'POST',
53-
headers: {
54-
'Content-Type': 'application/x-www-form-urlencoded',
55-
},
56-
body: new URLSearchParams({
57-
client_id: config.clientId,
58-
client_secret: config.clientSecret,
59-
grant_type: 'authorization_code',
60-
redirect_uri: config.redirectUrl,
61-
code: code as string,
62-
}).toString(),
63-
}).catch((error) => {
64-
return {error}
65-
})
62+
request.code_verifier = code_verifier
63+
}
64+
if (config.post_auth) {
65+
request.client_id = config.clientId
66+
request.client_secret = config.clientSecret
67+
}
68+
let headers: Record<string, string> = {
69+
'Content-Type': 'application/x-www-form-urlencoded',
70+
}
71+
if (!config.post_auth) {
72+
headers['Authorization'] = 'Basic ' + btoa(config.clientId + ":" + config.clientSecret)
73+
}
74+
75+
let tokens = await $fetch<AuthCodeResponse>(response.token_endpoint, {
76+
method: 'POST',
77+
headers: headers,
78+
body: new URLSearchParams(request).toString(),
79+
}).catch(error => {
80+
console.debug(errorToPOJO(error))
81+
return [error.code, error.data.error_description]
82+
})
83+
84+
if (Array.isArray(tokens)) {
85+
return createError({
86+
statusCode: tokens[0],
87+
message: `Login failed: '${tokens[1]}'. Please try again later or contact the website administrator`,
88+
});
6689
}
6790

6891
if (add_idp) {

server/utils/oidc.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export type IdpMetadata = {
2+
issuer: string,
3+
authorization_endpoint: string,
4+
token_endpoint: string,
5+
revocation_endpoint: string,
6+
introspection_endpoint: string,
7+
userinfo_endpoint: string,
8+
jwks_uri: string
9+
}
10+
11+
export type AuthQuery = {
12+
response_type: 'code',
13+
client_id: string,
14+
redirect_uri: string,
15+
scope: string,
16+
code_challenge_method?: 'S256',
17+
code_challenge?: string
18+
}
19+
20+
export function errorToPOJO(error: any) {
21+
const ret: any = {};
22+
for (const propertyName of Object.getOwnPropertyNames(error)) {
23+
ret[propertyName] = error[propertyName];
24+
}
25+
return ret;
26+
}
27+
28+
export const fetchCachedOidcMetadata = defineCachedFunction(async (wellKnowUrl: string): Promise<IdpMetadata | [number, string]> => {
29+
return await $fetch<IdpMetadata>(wellKnowUrl)
30+
.catch(error => {
31+
console.error(error)
32+
return [error.code, error.data.error_description]
33+
})
34+
}, {
35+
group: 'idp-configs',
36+
name: 'idp-metadata',
37+
maxAge: useRuntimeConfig().cacheMaxAge || 60 * 60, // Defaults to 1 hour
38+
swr: false,
39+
getKey: (identityProvider: string) => identityProvider,
40+
})

0 commit comments

Comments
 (0)