feat(oidc): enhance SSO target app resolution and audit logging
- Updated targetAppFromReturnTo function to handle OIDC authorization unwrap and added search parameter processing. - Integrated target app resolution into the OIDC route for improved audit logging of SSO handoffs. - Added a test case to verify the logging of the target app during the authorization process. - Updated documentation to reflect changes in audit logging for the Technitium DNS application. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -50,6 +50,66 @@ describe('OIDC IdP', () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('records auth.sso_handoff with target_app dns on authorize', async () => {
|
||||
const app = await buildTestApp()
|
||||
|
||||
const login = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/login',
|
||||
payload: { email: '[email protected]', password: 'adminpass' },
|
||||
})
|
||||
expect(login.statusCode).toBe(200)
|
||||
const token = (login.json() as { access_token: string }).access_token
|
||||
const refresh = login.cookies.find((c) => c.name === 'refresh_token')
|
||||
expect(refresh?.value).toBeTruthy()
|
||||
|
||||
const created = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/admin/oidc/clients',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
name: 'Technitium',
|
||||
redirect_uris: ['https://dns.test.local/sso/callback'],
|
||||
scopes: ['openid', 'profile', 'email', 'groups'],
|
||||
enabled: true,
|
||||
},
|
||||
})
|
||||
const client = created.json() as { client_id: string }
|
||||
|
||||
const authorize = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/oauth/authorize',
|
||||
cookies: { refresh_token: refresh!.value },
|
||||
query: {
|
||||
client_id: client.client_id,
|
||||
redirect_uri: 'https://dns.test.local/sso/callback',
|
||||
response_type: 'code',
|
||||
scope: 'openid profile email groups',
|
||||
state: 'xyz',
|
||||
},
|
||||
})
|
||||
expect(authorize.statusCode).toBe(302)
|
||||
|
||||
const audit = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/v1/admin/audit?kind=logins&limit=50',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(audit.statusCode).toBe(200)
|
||||
const entries = audit.json() as {
|
||||
action: string
|
||||
details: Record<string, unknown> | null
|
||||
}[]
|
||||
const handoff = entries.find(
|
||||
(e) =>
|
||||
e.action === 'auth.sso_handoff' &&
|
||||
e.details?.target_app === 'dns' &&
|
||||
e.details?.auth_mode === 'oidc',
|
||||
)
|
||||
expect(handoff).toBeTruthy()
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('reloads RS256 key from SQLite after restart (extractable import)', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'oidc-key-'))
|
||||
const dbPath = `sqlite:${join(dir, 'app.db')}`
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { targetAppFromReturnTo } from '../src/lib/target-app.js'
|
||||
|
||||
describe('targetAppFromReturnTo', () => {
|
||||
it('maps dns host and /sso/ path', () => {
|
||||
expect(targetAppFromReturnTo('https://dns.shnt.top/sso/callback')).toBe(
|
||||
'dns',
|
||||
)
|
||||
})
|
||||
|
||||
it('unwraps portal /oauth/authorize return_to via redirect_uri', () => {
|
||||
const returnTo =
|
||||
'https://auth.shnt.top/oauth/authorize?client_id=abc&redirect_uri=' +
|
||||
encodeURIComponent('https://dns.shnt.top/sso/callback') +
|
||||
'&response_type=code&scope=openid'
|
||||
expect(targetAppFromReturnTo(returnTo)).toBe('dns')
|
||||
})
|
||||
|
||||
it('keeps portal for bare issuer authorize without redirect_uri', () => {
|
||||
expect(
|
||||
targetAppFromReturnTo('https://auth.shnt.top/oauth/authorize'),
|
||||
).toBe('portal')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user