Skip to content

Flexible authentication #65

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

Merged
merged 14 commits into from
Jun 21, 2019
5 changes: 5 additions & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ pipeline {
}
}
}
post {
always {
cleanWs()
}
}
}
2 changes: 1 addition & 1 deletion docs/http4k.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ compile "nl.myndocs:oauth2-server-http4k:$myndocs_oauth_version"
```kotlin
val app: HttpHandler = routes(
"/ping" bind GET to { _: Request -> Response(Status.OK).body("pong!") }
) `enable oauth2` {
).enableOauth2 {
identityService = InMemoryIdentity()
.identity {
username = "foo"
Expand Down
71 changes: 38 additions & 33 deletions oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package nl.myndocs.oauth2

import nl.myndocs.oauth2.authenticator.Authorizer
import nl.myndocs.oauth2.authenticator.Credentials
import nl.myndocs.oauth2.exception.*
import nl.myndocs.oauth2.grant.Granter
import nl.myndocs.oauth2.grant.GrantingCall
Expand All @@ -11,16 +11,17 @@ import nl.myndocs.oauth2.request.CallContext
import nl.myndocs.oauth2.request.RedirectAuthorizationCodeRequest
import nl.myndocs.oauth2.request.RedirectTokenRequest
import nl.myndocs.oauth2.request.headerCaseInsensitive
import nl.myndocs.oauth2.router.RedirectRouter
import nl.myndocs.oauth2.router.RedirectRouterResponse

class CallRouter(
val tokenEndpoint: String,
val authorizeEndpoint: String,
val tokenInfoEndpoint: String,
private val tokenInfoCallback: (TokenInfo) -> Map<String, Any?>,
private val granters: List<GrantingCall.() -> Granter>,
private val grantingCallFactory: (CallContext) -> GrantingCall,
private val authorizerFactory: (CallContext) -> Authorizer
) {
private val grantingCallFactory: (CallContext) -> GrantingCall
) : RedirectRouter {
companion object {
const val METHOD_POST = "post"
const val METHOD_GET = "get"
Expand All @@ -33,11 +34,18 @@ class CallRouter(
fun route(callContext: CallContext) {
when (callContext.path) {
tokenEndpoint -> routeTokenEndpoint(callContext)
authorizeEndpoint -> routeAuthorizeEndpoint(callContext, authorizerFactory(callContext))
tokenInfoEndpoint -> routeTokenInfoEndpoint(callContext)
}
}

override fun route(callContext: CallContext, credentials: Credentials?): RedirectRouterResponse {
return when (callContext.path) {
authorizeEndpoint -> routeAuthorizeEndpoint(callContext, credentials)
else -> throw NoRoutesFoundException("Route '${callContext.path}' not found")
}
}


private fun routeTokenEndpoint(callContext: CallContext) {
if (callContext.method.toLowerCase() != METHOD_POST) {
return
Expand Down Expand Up @@ -71,21 +79,18 @@ class CallRouter(

fun routeAuthorizationCodeRedirect(
callContext: CallContext,
authorizer: Authorizer
) {
credentials: Credentials?
): RedirectRouterResponse {
val queryParameters = callContext.queryParameters
val credentials = authorizer.extractCredentials()
try {
val redirect = grantingCallFactory(callContext).redirect(
RedirectAuthorizationCodeRequest(
queryParameters["client_id"],
queryParameters["redirect_uri"],
credentials?.username ?: "",
credentials?.password ?: "",
credentials?.username,
credentials?.password,
queryParameters["scope"]
),
authorizer.authenticator(),
authorizer.scopesVerifier()
)
)

var stateQueryParameter = ""
Expand All @@ -95,31 +100,31 @@ class CallRouter(
}

callContext.redirect(queryParameters["redirect_uri"] + "?code=${redirect.codeToken}$stateQueryParameter")

return RedirectRouterResponse(true)
} catch (unverifiedIdentityException: InvalidIdentityException) {
callContext.respondStatus(STATUS_UNAUTHORIZED)
authorizer.failedAuthentication()

return RedirectRouterResponse(false)
}
}


fun routeAccessTokenRedirect(
callContext: CallContext,
authorizer: Authorizer
) {
credentials: Credentials?
): RedirectRouterResponse {
val queryParameters = callContext.queryParameters
val credentials = authorizer.extractCredentials()

try {
val redirect = grantingCallFactory(callContext).redirect(
RedirectTokenRequest(
queryParameters["client_id"],
queryParameters["redirect_uri"],
credentials?.username ?: "",
credentials?.password ?: "",
credentials?.username,
credentials?.password,
queryParameters["scope"]
),
authorizer.authenticator(),
authorizer.scopesVerifier()
)
)

var stateQueryParameter = ""
Expand All @@ -133,33 +138,33 @@ class CallRouter(
"&token_type=bearer&expires_in=${redirect.expiresIn()}$stateQueryParameter"
)

return RedirectRouterResponse(true)
} catch (unverifiedIdentityException: InvalidIdentityException) {
authorizer.failedAuthentication()
callContext.respondStatus(STATUS_UNAUTHORIZED)

return RedirectRouterResponse(false)
}
}

private fun routeAuthorizeEndpoint(callContext: CallContext, authorizer: Authorizer) {
private fun routeAuthorizeEndpoint(callContext: CallContext, credentials: Credentials?): RedirectRouterResponse {
try {
if (!arrayOf(METHOD_GET, METHOD_POST).contains(callContext.method.toLowerCase())) {
return
return RedirectRouterResponse(false)
}

val allowedResponseTypes = setOf("code", "token")
val responseType = callContext.queryParameters["response_type"]
?: throw InvalidRequestException("'response_type' not given")

if (!allowedResponseTypes.contains(responseType)) {
throw InvalidGrantException("'grant_type' with value '$responseType' not allowed")
}

when (responseType) {
"code" -> routeAuthorizationCodeRedirect(callContext, authorizer)
"token" -> routeAccessTokenRedirect(callContext, authorizer)
return when (responseType) {
"code" -> routeAuthorizationCodeRedirect(callContext, credentials)
"token" -> routeAccessTokenRedirect(callContext, credentials)
else -> throw InvalidGrantException("'grant_type' with value '$responseType' not allowed")
}
} catch (oauthException: OauthException) {
callContext.respondStatus(STATUS_BAD_REQUEST)
callContext.respondJson(oauthException.toMap())

return RedirectRouterResponse(false)
}
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package nl.myndocs.oauth2.config

import nl.myndocs.oauth2.CallRouter
import nl.myndocs.oauth2.authenticator.Authorizer
import nl.myndocs.oauth2.grant.*
import nl.myndocs.oauth2.identity.TokenInfo
import nl.myndocs.oauth2.request.CallContext
Expand All @@ -20,7 +19,7 @@ internal object CallRouterBuilder {
var granters: List<GrantingCall.() -> Granter> = listOf()
}

fun build(configuration: Configuration, grantingCallFactory: (CallContext) -> GrantingCall, authorizerFactory: (CallContext) -> Authorizer) = CallRouter(
fun build(configuration: Configuration, grantingCallFactory: (CallContext) -> GrantingCall) = CallRouter(
configuration.tokenEndpoint,
configuration.authorizeEndpoint,
configuration.tokenInfoEndpoint,
Expand All @@ -31,7 +30,6 @@ internal object CallRouterBuilder {
{ grantClientCredentials() },
{ grantRefreshToken() }
) + configuration.granters,
grantingCallFactory,
authorizerFactory
grantingCallFactory
)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package nl.myndocs.oauth2.config

import nl.myndocs.oauth2.CallRouter
import nl.myndocs.oauth2.authenticator.Authorizer
import nl.myndocs.oauth2.request.CallContext

data class Configuration(
val callRouter: CallRouter
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
package nl.myndocs.oauth2.config

import nl.myndocs.oauth2.authenticator.Authorizer
import nl.myndocs.oauth2.client.ClientService
import nl.myndocs.oauth2.grant.Granter
import nl.myndocs.oauth2.grant.GrantingCall
import nl.myndocs.oauth2.identity.IdentityService
import nl.myndocs.oauth2.identity.TokenInfo
import nl.myndocs.oauth2.request.CallContext
import nl.myndocs.oauth2.request.auth.BasicAuthorizer
import nl.myndocs.oauth2.response.AccessTokenResponder
import nl.myndocs.oauth2.response.DefaultAccessTokenResponder
import nl.myndocs.oauth2.token.TokenStore
import nl.myndocs.oauth2.token.converter.*

object ConfigurationBuilder {
class Configuration {
open class Configuration {
internal val callRouterConfiguration = CallRouterBuilder.Configuration()

var authorizationEndpoint: String
Expand Down Expand Up @@ -47,8 +45,6 @@ object ConfigurationBuilder {
callRouterConfiguration.granters = value
}

var authorizerFactory: (CallContext) -> Authorizer = ::BasicAuthorizer

var identityService: IdentityService? = null
var clientService: ClientService? = null
var tokenStore: TokenStore? = null
Expand All @@ -58,8 +54,7 @@ object ConfigurationBuilder {
var accessTokenResponder: AccessTokenResponder = DefaultAccessTokenResponder
}

fun build(configurer: Configuration.() -> Unit): nl.myndocs.oauth2.config.Configuration {
val configuration = Configuration()
fun build(configurer: Configuration.() -> Unit, configuration: Configuration): nl.myndocs.oauth2.config.Configuration {
configurer(configuration)

val grantingCallFactory: (CallContext) -> GrantingCall = { callContext ->
Expand All @@ -79,9 +74,13 @@ object ConfigurationBuilder {
return Configuration(
CallRouterBuilder.build(
configuration.callRouterConfiguration,
grantingCallFactory,
configuration.authorizerFactory
grantingCallFactory
)
)
}
fun build(configurer: Configuration.() -> Unit): nl.myndocs.oauth2.config.Configuration {
val configuration = Configuration()

return build(configurer, configuration)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nl.myndocs.oauth2.exception

class NoRoutesFoundException : Exception {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,13 @@ internal val INVALID_REQUEST_FIELD_MESSAGE = "'%s' field is missing"
fun GrantingCall.validateScopes(
client: Client,
identity: Identity,
requestedScopes: Set<String>,
identityScopeVerifier: IdentityScopeVerifier? = null) {
requestedScopes: Set<String>) {
val scopesAllowed = scopesAllowed(client.clientScopes, requestedScopes)
if (!scopesAllowed) {
throw InvalidScopeException(requestedScopes.minus(client.clientScopes))
}

val allowedScopes = identityScopeVerifier?.allowedScopes(client, identity, requestedScopes)
?: identityService.allowedScopes(client, identity, requestedScopes)
val allowedScopes = identityService.allowedScopes(client, identity, requestedScopes)

val ivalidScopes = requestedScopes.minus(allowedScopes)
if (ivalidScopes.isNotEmpty()) {
Expand Down
Loading