Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC stubbing #5

Merged
merged 6 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"mongodb": "6.0.0",
"node-fetch": "2.7.0",
"node-jose": "2.2.0",
"pem-jwk": "2.0.0",
"pino": "8.15.0"
},
"devDependencies": {
Expand Down
9 changes: 9 additions & 0 deletions src/api/admin/controllers/oidc-session-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { sessions } from '~/src/api/oidc/helpers/session-store'

const oidcSessionController = {
handler: async (request, h) => {
return h.response(sessions).code(200)
}
}

export { oidcSessionController }
6 changes: 6 additions & 0 deletions src/api/admin/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { triggerEcrPush } from '~/src/api/admin/controllers/trigger-ecr-push'
import { oidcSessionController } from '~/src/api/admin/controllers/oidc-session-controller'

const adminStub = {
plugin: {
Expand All @@ -9,6 +10,11 @@ const adminStub = {
method: 'POST',
path: '/_admin/trigger-ecr-push/{repo}/{tag}',
...triggerEcrPush
},
{
method: 'GET',
path: '/_admin/oidc/sessions',
...oidcSessionController
}
])
}
Expand Down
70 changes: 70 additions & 0 deletions src/api/oidc/controllers/authorize-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { oidcConfig } from '~/src/api/oidc/oidc-config'
import { validateScope } from '~/src/api/oidc/helpers/validate-scope'
import { newSession } from '~/src/api/oidc/helpers/session-store'
import { allUsers } from '~/src/api/oidc/helpers/users'
import { renderLoginPage } from '~/src/api/oidc/helpers/render-login-page'

const authorizeController = {
handler: (request, h) => {
// a bit of a hack, but basically if the user param hasn't been set
// show a 'login' page where they can select which fake user they want
if (request.query.user === undefined) {
return renderLoginPage(request.url, h)
}

// TODO check these are all set, use joi or something
const clientId = request.query.client_id
const responseType = request.query.response_type
const redirectUri = request.query.redirect_uri
const state = request.query.state
const scope = request.query.scope
const codeChallengeMethod = request.query.code_challenge_method

// validate request
const unsupportedScopes = validateScope(scope)
if (unsupportedScopes.length > 0) {
return h
.response(`Unsupported scopes ${unsupportedScopes.join(',')}`)
.code(400)
}

if (clientId !== oidcConfig.clientId) {
return h.response(`Invalid client id ${clientId}`).code(401)
}

if (!oidcConfig.responseTypesSupported.includes(responseType)) {
return h.response(`Unsupported response type ${responseType}`).code(400)
}

if (
codeChallengeMethod &&
!oidcConfig.codeChallengeMethodsSupported.includes(codeChallengeMethod)
) {
return h
.response(`Unsupported code_challenge_method ${codeChallengeMethod}`)
.code(400)
}

const user = allUsers[request.query.user]
if (user === undefined) {
request.logger.error(`Invalid user selected ${request.query.user}`)
return h.response(`Invalid user selection!`).code(400)
}

// create session
const session = newSession(
scope,
request.query.nonce,
user,
request.query.code_challenge,
request.query.code_challenge_method
)

const location = new URL(redirectUri)
location.searchParams.append('code', session.sessionId)
location.searchParams.append('state', state)
return h.redirect(location.toString())
}
}

export { authorizeController }
117 changes: 117 additions & 0 deletions src/api/oidc/controllers/token-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { oidcConfig } from '~/src/api/oidc/oidc-config'
import {
getSessionByToken,
sessions
} from '~/src/api/oidc/helpers/session-store'
import {
generateIDToken,
generateRefreshToken,
generateToken
} from '~/src/api/oidc/helpers/oidc-crypto'
import { validateCodeChallenge } from '~/src/api/oidc/helpers/validate-code-challenge'

const tokenController = {
handler: (request, h) => {
const logger = request.logger

const clientId = request.payload.client_id
const clientSecret = request.payload.client_secret
const grantType = request.payload.grant_type
const code = request.payload.code
const refreshToken = request.payload.refresh_token
const codeVerifier = request.payload.code_verifier

// validate client id
if (clientId !== oidcConfig.clientId) {
logger.error(`Invalid client id ${clientId}`)
return h.response(`Invalid client id ${clientId}`).code(401)
}

// validate secret
if (clientSecret !== oidcConfig.clientSecret) {
logger.error(`Invalid client secret`)
return h.response(`Invalid client secret`).code(401)
}

// get the session depending on the grant type
let result = null

if (grantType === 'authorization_code') {
logger.info('handling authorization code')
result = getSessionForAuthorizationCode(code)
const { valid, err } = validateCodeChallenge(result.session, codeVerifier)
if (!valid) {
logger.error(err)
return h.response(err).code(401)
}
} else if (grantType === 'refresh_token') {
logger.info('handling refresh token code')
result = getSessionForRefreshToken(refreshToken)
} else {
return h.response(`invalid grant type ${grantType}`).code(400)
}

const { session, valid } = result

if (!valid) {
logger.error('session was missing or invalid')
return h
.response(`invalid code/token for grant type ${grantType}`)
.code(400)
}

// build the token response

const tokenResponse = {
token_type: 'bearer',
expires_in: oidcConfig.ttl
}

// copy over the refresh token if its not there
// unsure if we still need the !== string 'null' check anymore
if (refreshToken && refreshToken !== 'null') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that's actually needed cause you handle the refresh token further down?

logger.info(`refresh token ${refreshToken}`)
tokenResponse.refresh_token = refreshToken
}

tokenResponse.access_token = generateToken(request.keys, session)

if (session.scopes[0] === 'openid') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. Similarly, you only generate a refresh token if scope offline is passed.

tokenResponse.id_token = generateIDToken(request.keys, session)
}

if (session.scopes[0] === 'refresh') {
logger.info('generating a refresh token')
tokenResponse.refresh_token = generateRefreshToken(request.keys, session)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check my comment above on offline scope

}

logger.info(tokenResponse)

return h
.response(tokenResponse)
.header('Cache-Control', 'no-cache, no-store, must-revalidate')
.code(200)
}
}

function getSessionForAuthorizationCode(code) {
const session = sessions[code]
if (!session || session.granted) {
return { session: null, valid: false }
}
sessions[code].granted = true
return {
session,
valid: true
}
}

function getSessionForRefreshToken(refreshToken) {
const session = getSessionByToken(refreshToken)
return {
session,
valid: session !== undefined
}
}

export { tokenController }
22 changes: 22 additions & 0 deletions src/api/oidc/controllers/user-info-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import jsonwebtoken from 'jsonwebtoken'

const userInfoController = {
handler: (request, h) => {
const authHeader = request.headers.authorization
if (authHeader === undefined || !authHeader.startsWith('Bearer ')) {
return h.response('Missing bearer token').code(401)
}
// drop 'Bearer '
const token = authHeader.slice(7)

try {
const decoded = jsonwebtoken.verify(token, request.keys.pem.publicKey)
return h.response(decoded).code(200)
} catch (e) {
request.logger.error(e)
return h.response('invalid token').code(401)
}
}
}

export { userInfoController }
Loading