All Downloads are FREE. Search and download functionalities are using the official Maven repository.

no.nav.security.mock.oauth2.http.OAuth2HttpRequestHandler.kt Maven / Gradle / Ivy

The newest version!
package no.nav.security.mock.oauth2.http

import com.nimbusds.oauth2.sdk.ErrorObject
import com.nimbusds.oauth2.sdk.GeneralException
import com.nimbusds.oauth2.sdk.GrantType
import com.nimbusds.oauth2.sdk.GrantType.AUTHORIZATION_CODE
import com.nimbusds.oauth2.sdk.GrantType.CLIENT_CREDENTIALS
import com.nimbusds.oauth2.sdk.GrantType.JWT_BEARER
import com.nimbusds.oauth2.sdk.GrantType.PASSWORD
import com.nimbusds.oauth2.sdk.GrantType.REFRESH_TOKEN
import com.nimbusds.oauth2.sdk.OAuth2Error
import com.nimbusds.oauth2.sdk.ParseException
import com.nimbusds.openid.connect.sdk.AuthenticationRequest
import mu.KotlinLogging
import no.nav.security.mock.oauth2.OAuth2Config
import no.nav.security.mock.oauth2.OAuth2Exception
import no.nav.security.mock.oauth2.debugger.DebuggerRequestHandler
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.AUTHORIZATION
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.END_SESSION
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.JWKS
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.OAUTH2_WELL_KNOWN
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.OIDC_WELL_KNOWN
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.REVOKE
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.TOKEN
import no.nav.security.mock.oauth2.extensions.isPrompt
import no.nav.security.mock.oauth2.extensions.issuerId
import no.nav.security.mock.oauth2.extensions.toIssuerUrl
import no.nav.security.mock.oauth2.grant.AuthorizationCodeHandler
import no.nav.security.mock.oauth2.grant.ClientCredentialsGrantHandler
import no.nav.security.mock.oauth2.grant.GrantHandler
import no.nav.security.mock.oauth2.grant.JwtBearerGrantHandler
import no.nav.security.mock.oauth2.grant.PasswordGrantHandler
import no.nav.security.mock.oauth2.grant.RefreshToken
import no.nav.security.mock.oauth2.grant.RefreshTokenGrantHandler
import no.nav.security.mock.oauth2.grant.RefreshTokenManager
import no.nav.security.mock.oauth2.grant.TOKEN_EXCHANGE
import no.nav.security.mock.oauth2.grant.TokenExchangeGrantHandler
import no.nav.security.mock.oauth2.introspect.introspect
import no.nav.security.mock.oauth2.invalidGrant
import no.nav.security.mock.oauth2.login.Login
import no.nav.security.mock.oauth2.login.LoginRequestHandler
import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback
import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
import no.nav.security.mock.oauth2.userinfo.userInfo
import okhttp3.Headers
import java.io.File
import java.net.URLEncoder
import java.nio.charset.Charset
import java.nio.file.Files
import java.nio.file.Paths
import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingQueue

private val log = KotlinLogging.logger {}

class OAuth2HttpRequestHandler(
    private val config: OAuth2Config,
) {
    private val loginRequestHandler = LoginRequestHandler(templateMapper, config)
    private val debuggerRequestHandler = DebuggerRequestHandler(ssl = config.httpServer.sslConfig())
    private val tokenCallbackQueue: BlockingQueue = LinkedBlockingQueue()
    private val refreshTokenManager = RefreshTokenManager()

    private val grantHandlers: Map =
        mapOf(
            AUTHORIZATION_CODE to AuthorizationCodeHandler(config.tokenProvider, refreshTokenManager),
            CLIENT_CREDENTIALS to ClientCredentialsGrantHandler(config.tokenProvider),
            JWT_BEARER to JwtBearerGrantHandler(config.tokenProvider),
            TOKEN_EXCHANGE to TokenExchangeGrantHandler(config.tokenProvider),
            REFRESH_TOKEN to RefreshTokenGrantHandler(config.tokenProvider, refreshTokenManager, config.rotateRefreshToken),
            PASSWORD to PasswordGrantHandler(config.tokenProvider),
        )

    private val exceptionHandler: ExceptionHandler = { request, error ->
        log.error("received exception when handling request: ${request.url}.", error)
        val msg = URLEncoder.encode(error.message, Charset.forName("UTF-8"))
        val errorObject: ErrorObject =
            when (error) {
                is OAuth2Exception -> error.errorObject
                is ParseException -> error.errorObject ?: OAuth2Error.INVALID_REQUEST.setDescription("failed to parse request: $msg")
                is GeneralException -> error.errorObject
                else -> null
            } ?: OAuth2Error.SERVER_ERROR.setDescription("unexpected exception with message: $msg")
        oauth2Error(errorObject)
    }

    val authorizationServer: Route =
        routes {
            exceptionHandler(exceptionHandler)
            interceptors(CorsInterceptor())
            wellKnown()
            jwks()
            authorization()
            token()
            endSession()
            revocation(refreshTokenManager)
            userInfo(config.tokenProvider)
            introspect(config.tokenProvider)
            preflight()
            staticAssets()
            get("/favicon.ico") { OAuth2HttpResponse(status = 200) }
            attach(debuggerRequestHandler)
        }

    fun enqueueTokenCallback(oAuth2TokenCallback: OAuth2TokenCallback) = tokenCallbackQueue.add(oAuth2TokenCallback)

    private fun Route.Builder.wellKnown() =
        get(OIDC_WELL_KNOWN, OAUTH2_WELL_KNOWN) {
            log.debug("returning well-known json data for url=${it.url}")
            json(it.toWellKnown())
        }

    private fun Route.Builder.jwks() =
        get(JWKS) {
            log.debug("handle jwks request on url=${it.url}")
            val issuerId = it.url.issuerId()
            val jwkSet = config.tokenProvider.publicJwkSet(issuerId)
            json(jwkSet.toJSONObject())
        }

    private fun Route.Builder.authorization() =
        apply {
            val authorizationCodeHandler = grantHandlers[AUTHORIZATION_CODE] as AuthorizationCodeHandler
            get(AUTHORIZATION) {
                val authRequest: AuthenticationRequest = it.asAuthenticationRequest()
                if (config.interactiveLogin || authRequest.isPrompt()) {
                    html(loginRequestHandler.loginHtml(it))
                } else {
                    authenticationSuccess(authorizationCodeHandler.authorizationCodeResponse(authRequest))
                }
            }
            post(AUTHORIZATION) {
                val authRequest: AuthenticationRequest = it.asAuthenticationRequest()
                val login: Login = loginRequestHandler.loginSubmit(it)
                authenticationSuccess(authorizationCodeHandler.authorizationCodeResponse(authRequest, login))
            }
        }

    private fun Route.Builder.endSession() =
        any(END_SESSION) {
            log.debug("handle end session request $it")
            it.url.queryParameter("post_logout_redirect_uri")?.let { postLogoutRedirectUri ->
                it.url.queryParameter("state")?.let { state ->
                    redirect("$postLogoutRedirectUri?state=$state")
                } ?: redirect(postLogoutRedirectUri)
            } ?: html("logged out")
        }

    private fun Route.Builder.revocation(refreshTokenManager: RefreshTokenManager) =
        post(REVOKE) {
            log.debug("handle revocation request $it")
            when (val hint = it.formParameters.get("token_type_hint")) {
                "refresh_token" -> {
                    val token = it.formParameters.get("token") as RefreshToken
                    refreshTokenManager.remove(token)
                }

                else -> throw OAuth2Exception(
                    ErrorObject("unsupported_token_type", "unsupported token type: $hint", 400),
                    "unsupported token type: $hint",
                )
            }
            OAuth2HttpResponse(status = 200, body = "ok")
        }

    private fun Route.Builder.token() =
        apply {
            get(TOKEN) {
                OAuth2HttpResponse(status = 405, body = "unsupported method")
            }
            post(TOKEN) {
                log.debug("handle token request $it")
                val grantType = it.grantType()
                val tokenCallback: OAuth2TokenCallback = tokenCallbackFromQueueOrDefault(it.url.issuerId())
                val grantHandler: GrantHandler = grantHandlers[grantType] ?: invalidGrant(grantType)
                val tokenResponse = grantHandler.tokenResponse(it, it.url.toIssuerUrl(), tokenCallback)
                json(tokenResponse)
            }
        }

    private fun Route.Builder.staticAssets() =
        apply {
            if (config.staticAssetsPath != null) {
                get("/static/*") {
                    val path =
                        it.url.pathSegments
                            .drop(1)
                            .joinToString("/")
                    val normalized = Paths.get(path).normalize().toString()
                    val file = File(config.staticAssetsPath, normalized)

                    if (file.canonicalPath.startsWith(config.staticAssetsPath) && file.exists()) {
                        val contentType = Files.probeContentType(file.toPath()) ?: "application/octet-stream"
                        OAuth2HttpResponse(status = 200, bytesBody = file.readBytes(), headers = Headers.headersOf("Content-Type", contentType))
                    } else {
                        OAuth2HttpResponse(status = 404, body = "not found")
                    }
                }
            }
        }

    private fun Route.Builder.preflight() = options { OAuth2HttpResponse(status = 204) }

    private fun tokenCallbackFromQueueOrDefault(issuerId: String): OAuth2TokenCallback =
        when (issuerId) {
            tokenCallbackQueue.peek()?.issuerId() -> tokenCallbackQueue.take()
            else -> {
                config.tokenCallbacks.firstOrNull { it.issuerId() == issuerId } ?: DefaultOAuth2TokenCallback(issuerId = issuerId)
            }
        }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy