From ad84914a101fe49e717a958e34e554bedeb2e994 Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 16 Dec 2018 21:00:41 +0100 Subject: [PATCH 1/6] 0.3.2-SNAPSHOT --- oauth2-server-client-inmemory/pom.xml | 2 +- oauth2-server-core/pom.xml | 2 +- oauth2-server-http4k/pom.xml | 2 +- oauth2-server-identity-inmemory/pom.xml | 2 +- oauth2-server-javalin/pom.xml | 2 +- oauth2-server-json/pom.xml | 2 +- oauth2-server-ktor/pom.xml | 2 +- oauth2-server-sparkjava/pom.xml | 2 +- oauth2-server-token-store-inmemory/pom.xml | 2 +- pom.xml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/oauth2-server-client-inmemory/pom.xml b/oauth2-server-client-inmemory/pom.xml index 091d177..e8b1011 100644 --- a/oauth2-server-client-inmemory/pom.xml +++ b/oauth2-server-client-inmemory/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.1 + 0.3.2-SNAPSHOT 4.0.0 diff --git a/oauth2-server-core/pom.xml b/oauth2-server-core/pom.xml index 6861d77..f4dbd2f 100644 --- a/oauth2-server-core/pom.xml +++ b/oauth2-server-core/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.1 + 0.3.2-SNAPSHOT 4.0.0 diff --git a/oauth2-server-http4k/pom.xml b/oauth2-server-http4k/pom.xml index c32cb6e..7f8466a 100644 --- a/oauth2-server-http4k/pom.xml +++ b/oauth2-server-http4k/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.1 + 0.3.2-SNAPSHOT 4.0.0 diff --git a/oauth2-server-identity-inmemory/pom.xml b/oauth2-server-identity-inmemory/pom.xml index fb31465..cc814a1 100644 --- a/oauth2-server-identity-inmemory/pom.xml +++ b/oauth2-server-identity-inmemory/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.1 + 0.3.2-SNAPSHOT 4.0.0 diff --git a/oauth2-server-javalin/pom.xml b/oauth2-server-javalin/pom.xml index 23e2f92..6e8eb00 100644 --- a/oauth2-server-javalin/pom.xml +++ b/oauth2-server-javalin/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.1 + 0.3.2-SNAPSHOT 4.0.0 diff --git a/oauth2-server-json/pom.xml b/oauth2-server-json/pom.xml index 8630d82..be15c53 100644 --- a/oauth2-server-json/pom.xml +++ b/oauth2-server-json/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.1 + 0.3.2-SNAPSHOT 4.0.0 diff --git a/oauth2-server-ktor/pom.xml b/oauth2-server-ktor/pom.xml index d5c82c3..446867b 100644 --- a/oauth2-server-ktor/pom.xml +++ b/oauth2-server-ktor/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.1 + 0.3.2-SNAPSHOT 4.0.0 diff --git a/oauth2-server-sparkjava/pom.xml b/oauth2-server-sparkjava/pom.xml index bc79622..d8208a4 100644 --- a/oauth2-server-sparkjava/pom.xml +++ b/oauth2-server-sparkjava/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.1 + 0.3.2-SNAPSHOT 4.0.0 diff --git a/oauth2-server-token-store-inmemory/pom.xml b/oauth2-server-token-store-inmemory/pom.xml index ae0d9f3..15c869d 100644 --- a/oauth2-server-token-store-inmemory/pom.xml +++ b/oauth2-server-token-store-inmemory/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.1 + 0.3.2-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index e68ef61..d3bd6c0 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ nl.myndocs kotlin-oauth2-server pom - 0.3.1 + 0.3.2-SNAPSHOT 1.3.0 From c0a859c917e6e22c3ae7c955e4020cdff25f840c Mon Sep 17 00:00:00 2001 From: "A. Bos" Date: Thu, 10 Jan 2019 21:46:51 +0100 Subject: [PATCH 2/6] Grant extension (#45) * Allow grant extension through extendable GrantingCall * Fix infinite loop * Granters should be configurable * Immutable granter map --- .../main/java/nl/myndocs/oauth2/CallRouter.kt | 89 ++++++------------- .../oauth2/config/CallRouterBuilder.kt | 13 ++- .../oauth2/config/ConfigurationBuilder.kt | 8 ++ .../myndocs/oauth2/grant/CallRouterDefault.kt | 53 +++++++++++ .../nl/myndocs/oauth2/grant/GrantBuilder.kt | 3 + .../java/nl/myndocs/oauth2/grant/Granter.kt | 6 ++ .../nl/myndocs/oauth2/grant/GrantingCall.kt | 9 ++ 7 files changed, 114 insertions(+), 67 deletions(-) create mode 100644 oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterDefault.kt create mode 100644 oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantBuilder.kt create mode 100644 oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/Granter.kt create mode 100644 oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantingCall.kt diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt index eb1cd36..a0e526e 100644 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt @@ -1,21 +1,22 @@ package nl.myndocs.oauth2 import nl.myndocs.oauth2.authenticator.Authorizer -import nl.myndocs.oauth2.client.AuthorizedGrantType.AUTHORIZATION_CODE -import nl.myndocs.oauth2.client.AuthorizedGrantType.CLIENT_CREDENTIALS -import nl.myndocs.oauth2.client.AuthorizedGrantType.PASSWORD -import nl.myndocs.oauth2.client.AuthorizedGrantType.REFRESH_TOKEN import nl.myndocs.oauth2.exception.* +import nl.myndocs.oauth2.grant.Granter +import nl.myndocs.oauth2.grant.GrantingCall import nl.myndocs.oauth2.identity.TokenInfo -import nl.myndocs.oauth2.request.* -import nl.myndocs.oauth2.token.toMap +import nl.myndocs.oauth2.request.CallContext +import nl.myndocs.oauth2.request.RedirectAuthorizationCodeRequest +import nl.myndocs.oauth2.request.RedirectTokenRequest +import nl.myndocs.oauth2.request.headerCaseInsensitive class CallRouter( private val tokenService: TokenService, val tokenEndpoint: String, val authorizeEndpoint: String, val tokenInfoEndpoint: String, - private val tokenInfoCallback: (TokenInfo) -> Map + private val tokenInfoCallback: (TokenInfo) -> Map, + private val granters: List Granter> ) { companion object { const val METHOD_POST = "post" @@ -42,76 +43,36 @@ class CallRouter( } try { - val allowedGrantTypes = setOf(PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, CLIENT_CREDENTIALS) val grantType = callContext.formParameters["grant_type"] ?: throw InvalidRequestException("'grant_type' not given") + val grantingCall = object: GrantingCall { + override val callContext: CallContext + get() = callContext + + override val tokenService = this@CallRouter.tokenService + } + + val granterMap = granters + .map { + val granter = grantingCall.it() + granter.grantType to granter + } + .toMap() + + val allowedGrantTypes = granterMap.keys + if (!allowedGrantTypes.contains(grantType)) { throw InvalidGrantException("'grant_type' with value '$grantType' not allowed") } - when (grantType) { - "password" -> routePasswordGrant(callContext, tokenService) - "authorization_code" -> routeAuthorizationCodeGrant(callContext, tokenService) - "refresh_token" -> routeRefreshTokenGrant(callContext, tokenService) - "client_credentials" -> routeClientCredentialsGrant(callContext, tokenService) - } + granterMap[grantType]!!.callback.invoke() } catch (oauthException: OauthException) { callContext.respondStatus(STATUS_BAD_REQUEST) callContext.respondJson(oauthException.toMap()) } } - fun routePasswordGrant(callContext: CallContext, tokenService: TokenService) { - val tokenResponse = tokenService.authorize( - PasswordGrantRequest( - callContext.formParameters["client_id"], - callContext.formParameters["client_secret"], - callContext.formParameters["username"], - callContext.formParameters["password"], - callContext.formParameters["scope"] - ) - ) - - callContext.respondJson(tokenResponse.toMap()) - } - - fun routeClientCredentialsGrant(callContext: CallContext, tokenService: TokenService) { - val tokenResponse = tokenService.authorize(ClientCredentialsRequest( - callContext.formParameters["client_id"], - callContext.formParameters["client_secret"], - callContext.formParameters["scope"] - )) - - callContext.respondJson(tokenResponse.toMap()) - } - - fun routeRefreshTokenGrant(callContext: CallContext, tokenService: TokenService) { - val accessToken = tokenService.refresh( - RefreshTokenRequest( - callContext.formParameters["client_id"], - callContext.formParameters["client_secret"], - callContext.formParameters["refresh_token"] - ) - ) - - callContext.respondJson(accessToken.toMap()) - } - - fun routeAuthorizationCodeGrant(callContext: CallContext, tokenService: TokenService) { - val accessToken = tokenService.authorize( - AuthorizationCodeRequest( - callContext.formParameters["client_id"], - callContext.formParameters["client_secret"], - callContext.formParameters["code"], - callContext.formParameters["redirect_uri"] - ) - ) - - callContext.respondJson(accessToken.toMap()) - } - - fun routeAuthorizationCodeRedirect( callContext: CallContext, tokenService: TokenService, diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/CallRouterBuilder.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/CallRouterBuilder.kt index 33732cb..710ef13 100644 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/CallRouterBuilder.kt +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/CallRouterBuilder.kt @@ -1,7 +1,7 @@ package nl.myndocs.oauth2.config -import nl.myndocs.oauth2.CallRouter -import nl.myndocs.oauth2.TokenService +import nl.myndocs.oauth2.* +import nl.myndocs.oauth2.grant.* import nl.myndocs.oauth2.identity.TokenInfo internal object CallRouterBuilder { @@ -16,6 +16,7 @@ internal object CallRouterBuilder { ).filterValues { it != null } } var tokenService: TokenService? = null + var granters: List Granter> = listOf() } fun build(configurer: Configuration.() -> Unit): CallRouter { @@ -30,6 +31,12 @@ internal object CallRouterBuilder { configuration.tokenEndpoint, configuration.authorizeEndpoint, configuration.tokenInfoEndpoint, - configuration.tokenInfoCallback + configuration.tokenInfoCallback, + listOf Granter>( + { grantPassword() }, + { grantAuthorizationCode() }, + { grantClientCredentials() }, + { grantRefreshToken() } + ) + configuration.granters ) } \ No newline at end of file diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/ConfigurationBuilder.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/ConfigurationBuilder.kt index e73177f..0f69b0e 100644 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/ConfigurationBuilder.kt +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/ConfigurationBuilder.kt @@ -2,6 +2,8 @@ package nl.myndocs.oauth2.config import nl.myndocs.oauth2.TokenService import nl.myndocs.oauth2.authenticator.Authorizer +import nl.myndocs.oauth2.grant.Granter +import nl.myndocs.oauth2.grant.GrantingCall import nl.myndocs.oauth2.identity.TokenInfo import nl.myndocs.oauth2.request.CallContext import nl.myndocs.oauth2.request.auth.BasicAuthorizer @@ -40,6 +42,12 @@ object ConfigurationBuilder { callRouterConfiguration.tokenInfoCallback = value } + var granters: List Granter> + get() = callRouterConfiguration.granters + set(value) { + callRouterConfiguration.granters = value + } + var authorizerFactory: (CallContext) -> Authorizer = ::BasicAuthorizer } diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterDefault.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterDefault.kt new file mode 100644 index 0000000..d3ad54c --- /dev/null +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterDefault.kt @@ -0,0 +1,53 @@ +package nl.myndocs.oauth2.grant + +import nl.myndocs.oauth2.request.* +import nl.myndocs.oauth2.token.toMap + +fun GrantingCall.grantPassword() = granter("password") { + val tokenResponse = tokenService.authorize( + PasswordGrantRequest( + callContext.formParameters["client_id"], + callContext.formParameters["client_secret"], + callContext.formParameters["username"], + callContext.formParameters["password"], + callContext.formParameters["scope"] + ) + ) + + callContext.respondJson(tokenResponse.toMap()) +} + +fun GrantingCall.grantClientCredentials() = granter("client_credentials") { + val tokenResponse = tokenService.authorize(ClientCredentialsRequest( + callContext.formParameters["client_id"], + callContext.formParameters["client_secret"], + callContext.formParameters["scope"] + )) + + callContext.respondJson(tokenResponse.toMap()) +} + +fun GrantingCall.grantRefreshToken() = granter("refresh_token") { + val accessToken = tokenService.refresh( + RefreshTokenRequest( + callContext.formParameters["client_id"], + callContext.formParameters["client_secret"], + callContext.formParameters["refresh_token"] + ) + ) + + callContext.respondJson(accessToken.toMap()) +} + +fun GrantingCall.grantAuthorizationCode() = granter("authorization_code") { + val accessToken = tokenService.authorize( + AuthorizationCodeRequest( + callContext.formParameters["client_id"], + callContext.formParameters["client_secret"], + callContext.formParameters["code"], + callContext.formParameters["redirect_uri"] + ) + ) + + callContext.respondJson(accessToken.toMap()) +} diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantBuilder.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantBuilder.kt new file mode 100644 index 0000000..d82c53a --- /dev/null +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantBuilder.kt @@ -0,0 +1,3 @@ +package nl.myndocs.oauth2.grant + +fun granter(grantType: String, callback: () -> Unit) = Granter(grantType, callback) \ No newline at end of file diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/Granter.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/Granter.kt new file mode 100644 index 0000000..929b830 --- /dev/null +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/Granter.kt @@ -0,0 +1,6 @@ +package nl.myndocs.oauth2.grant + +class Granter( + val grantType: String, + val callback: () -> Unit +) \ No newline at end of file diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantingCall.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantingCall.kt new file mode 100644 index 0000000..046d1d5 --- /dev/null +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantingCall.kt @@ -0,0 +1,9 @@ +package nl.myndocs.oauth2.grant + +import nl.myndocs.oauth2.TokenService +import nl.myndocs.oauth2.request.CallContext + +interface GrantingCall { + val callContext: CallContext + val tokenService: TokenService +} \ No newline at end of file From c12ce4164d3df4a15f4b23bbde8ef95163e1fccb Mon Sep 17 00:00:00 2001 From: "A. Bos" Date: Mon, 14 Jan 2019 15:43:40 +0100 Subject: [PATCH 3/6] Improve usability extension grants (#47) * Split TokenService into smaller pieces * Update documentation * Split CallRouter functions --- README.md | 225 ++--------- docs/http4k.md | 39 ++ docs/javalin.md | 37 ++ docs/ktor.md | 38 ++ docs/sparkjava.md | 35 ++ .../main/java/nl/myndocs/oauth2/CallRouter.kt | 25 +- .../nl/myndocs/oauth2/Oauth2TokenService.kt | 354 ------------------ .../java/nl/myndocs/oauth2/TokenService.kt | 33 -- .../oauth2/config/CallRouterBuilder.kt | 17 +- .../nl/myndocs/oauth2/config/Configuration.kt | 2 - .../oauth2/config/ConfigurationBuilder.kt | 34 +- .../config/Oauth2TokenServiceBuilder.kt | 32 -- .../oauth2/grant/CallRouterAuthorize.kt | 126 +++++++ .../myndocs/oauth2/grant/CallRouterDefault.kt | 76 +++- .../oauth2/grant/CallRouterRedirect.kt | 135 +++++++ .../myndocs/oauth2/grant/CallRouterRefresh.kt | 41 ++ .../nl/myndocs/oauth2/grant/GrantingCall.kt | 10 +- .../oauth2/token/converter/Converters.kt | 7 + .../AuthorizationCodeGrantTokenServiceTest.kt | 40 +- .../ClientCredentialsTokenServiceTest.kt | 35 +- .../oauth2/PasswordGrantTokenServiceTest.kt | 45 ++- .../RefreshTokenGrantTokenServiceTest.kt | 37 +- 22 files changed, 716 insertions(+), 707 deletions(-) create mode 100644 docs/http4k.md create mode 100644 docs/javalin.md create mode 100644 docs/ktor.md create mode 100644 docs/sparkjava.md delete mode 100644 oauth2-server-core/src/main/java/nl/myndocs/oauth2/Oauth2TokenService.kt delete mode 100644 oauth2-server-core/src/main/java/nl/myndocs/oauth2/TokenService.kt delete mode 100644 oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/Oauth2TokenServiceBuilder.kt create mode 100644 oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterAuthorize.kt create mode 100644 oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterRedirect.kt create mode 100644 oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterRefresh.kt create mode 100644 oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/converter/Converters.kt diff --git a/README.md b/README.md index c8c33d6..e05c11c 100644 --- a/README.md +++ b/README.md @@ -51,210 +51,27 @@ For the frameworks examples we need at least the following dependencies: ``` -## Ktor -The following dependency is required along with the dependencies described in Setup - -```xml - - nl.myndocs - oauth2-server-ktor - ${myndocs.oauth.version} - -``` - -In memory example for Ktor: -```kotlin -embeddedServer(Netty, 8080) { - install(Oauth2ServerFeature) { - tokenService = Oauth2TokenServiceBuilder.build { - identityService = InMemoryIdentity() - .identity { - username = "foo" - password = "bar" - } - clientService = InMemoryClient() - .client { - clientId = "testapp" - clientSecret = "testpass" - scopes = setOf("trusted") - redirectUris = setOf("https://localhost:8080/callback") - authorizedGrantTypes = setOf( - AuthorizedGrantType.AUTHORIZATION_CODE, - AuthorizedGrantType.PASSWORD, - AuthorizedGrantType.IMPLICIT, - AuthorizedGrantType.REFRESH_TOKEN - ) - } - tokenStore = InMemoryTokenStore() - } - } -}.start(wait = true) -``` - -## Javalin -The following dependency is required along with the dependencies described in Setup -```xml - - nl.myndocs - oauth2-server-javalin - ${myndocs.oauth.version} - -``` - -In memory example for Javalin: -```kotlin -Javalin.create().apply { - enableOauthServer { - tokenService = Oauth2TokenServiceBuilder.build { - identityService = InMemoryIdentity() - .identity { - username = "foo" - password = "bar" - } - clientService = InMemoryClient() - .client { - clientId = "testapp" - clientSecret = "testpass" - scopes = setOf("trusted") - redirectUris = setOf("https://localhost:7000/callback") - authorizedGrantTypes = setOf( - AuthorizedGrantType.AUTHORIZATION_CODE, - AuthorizedGrantType.PASSWORD, - AuthorizedGrantType.IMPLICIT, - AuthorizedGrantType.REFRESH_TOKEN - ) - } - tokenStore = InMemoryTokenStore() - } - - } -}.start(7000) -``` - -## Spark java -The following dependency is required along with the dependencies described in Setup -```xml - - nl.myndocs - oauth2-server-sparkjava - ${myndocs.oauth.version} - -``` - -In memory example for Spark java: -```kotlin -Oauth2Server.configureOauth2Server { - tokenService = Oauth2TokenServiceBuilder.build { - identityService = InMemoryIdentity() - .identity { - username = "foo" - password = "bar" - } - clientService = InMemoryClient() - .client { - clientId = "testapp" - clientSecret = "testpass" - scopes = setOf("trusted") - redirectUris = setOf("https://localhost:4567/callback") - authorizedGrantTypes = setOf( - AuthorizedGrantType.AUTHORIZATION_CODE, - AuthorizedGrantType.PASSWORD, - AuthorizedGrantType.IMPLICIT, - AuthorizedGrantType.REFRESH_TOKEN - ) - } - tokenStore = InMemoryTokenStore() - } -} -``` -## http4k -The following dependency is required along with the dependencies described in Setup -```xml - - nl.myndocs - oauth2-server-http4k - ${myndocs.oauth.version} - -``` - -In memory example for http4k: -```kotlin -val app: HttpHandler = routes( - "/ping" bind GET to { _: Request -> Response(Status.OK).body("pong!") } - ) `enable oauth2` { - tokenService = Oauth2TokenServiceBuilder.build { - identityService = InMemoryIdentity() - .identity { - username = "foo" - password = "bar" - } - clientService = InMemoryClient() - .client { - clientId = "testapp" - clientSecret = "testpass" - scopes = setOf("trusted") - redirectUris = setOf("http://localhost:8080/callback") - authorizedGrantTypes = setOf( - AuthorizedGrantType.AUTHORIZATION_CODE, - AuthorizedGrantType.PASSWORD, - AuthorizedGrantType.IMPLICIT, - AuthorizedGrantType.REFRESH_TOKEN - ) - } - tokenStore = InMemoryTokenStore() - } - } - - app.asServer(Jetty(9000)).start() -``` - -**Note:** `/ping` is only added for demonstration for own defined routes. -# Custom implementation -## Identity service -Users can be authenticate through the identity service. In OAuth2 terms this would be the resource owner. - -```kotlin -fun identityOf(forClient: Client, username: String): Identity? - -fun validCredentials(forClient: Client, identity: Identity, password: String): Boolean - -fun allowedScopes(forClient: Client, identity: Identity, scopes: Set): Set -``` - -Each of the methods that needs to be implemented contains `Client`. This could give you extra flexibility. -For example you could have user base per client, instead of have users over all clients. - -## Client service -Client service is similar to the identity service. - -```kotlin -fun clientOf(clientId: String): Client? - -fun validClient(client: Client, clientSecret: String): Boolean -``` - -## Token store -The following methods have to be implemented for a token store. - +### Framework implementation +The following frameworks are supported: +- [Ktor](docs/ktor.md) +- [Javalin](docs/javalin.md) +- [http4k](docs/http4k.md) +- [Sparkjava](docs/sparkjava.md) + +## Configuration +### Routing +Default endpoints are configured: + +| Type | Relative url | +| ----- | ------------- | +| token | /oauth/token | +| authorize | /oauth/authorize | +| token info | /oauth/tokeninfo | + +These values can be overridden: ```kotlin -fun storeAccessToken(accessToken: AccessToken) - -fun accessToken(token: String): AccessToken? - -fun revokeAccessToken(token: String) - -fun storeCodeToken(codeToken: CodeToken) - -fun codeToken(token: String): CodeToken? - -fun consumeCodeToken(token: String): CodeToken? - -fun storeRefreshToken(refreshToken: RefreshToken) - -fun refreshToken(token: String): RefreshToken? - -fun revokeRefreshToken(token: String) - +tokenEndpoint = "/custom/token" +authorizationEndpoint = "/custom/authorize" +tokenInfoEndpoint = "/custom/tokeninfo" ``` -When `AccessToken` is passed to `storeAccessToken` and it contains a `RefreshToken`, then `storeAccessToken` is also responsible for saving the refresh token. diff --git a/docs/http4k.md b/docs/http4k.md new file mode 100644 index 0000000..e20f2f3 --- /dev/null +++ b/docs/http4k.md @@ -0,0 +1,39 @@ +# http4k + +## Dependencies +```xml + + nl.myndocs + oauth2-server-http4k + ${myndocs.oauth.version} + +``` + +## Implementation +```kotlin +val app: HttpHandler = routes( + "/ping" bind GET to { _: Request -> Response(Status.OK).body("pong!") } + ) `enable oauth2` { + identityService = InMemoryIdentity() + .identity { + username = "foo" + password = "bar" + } + clientService = InMemoryClient() + .client { + clientId = "testapp" + clientSecret = "testpass" + scopes = setOf("trusted") + redirectUris = setOf("http://localhost:8080/callback") + authorizedGrantTypes = setOf( + AuthorizedGrantType.AUTHORIZATION_CODE, + AuthorizedGrantType.PASSWORD, + AuthorizedGrantType.IMPLICIT, + AuthorizedGrantType.REFRESH_TOKEN + ) + } + tokenStore = InMemoryTokenStore() + } + + app.asServer(Jetty(9000)).start() +``` diff --git a/docs/javalin.md b/docs/javalin.md new file mode 100644 index 0000000..2c9c065 --- /dev/null +++ b/docs/javalin.md @@ -0,0 +1,37 @@ +# Javalin + +## Dependencies +```xml + + nl.myndocs + oauth2-server-javalin + ${myndocs.oauth.version} + +``` + +## Implementation +```kotlin +Javalin.create().apply { + enableOauthServer { + identityService = InMemoryIdentity() + .identity { + username = "foo" + password = "bar" + } + clientService = InMemoryClient() + .client { + clientId = "testapp" + clientSecret = "testpass" + scopes = setOf("trusted") + redirectUris = setOf("https://localhost:7000/callback") + authorizedGrantTypes = setOf( + AuthorizedGrantType.AUTHORIZATION_CODE, + AuthorizedGrantType.PASSWORD, + AuthorizedGrantType.IMPLICIT, + AuthorizedGrantType.REFRESH_TOKEN + ) + } + tokenStore = InMemoryTokenStore() + } +}.start(7000) +``` diff --git a/docs/ktor.md b/docs/ktor.md new file mode 100644 index 0000000..31cb28d --- /dev/null +++ b/docs/ktor.md @@ -0,0 +1,38 @@ +# Ktor + +## Dependencies + +```xml + + nl.myndocs + oauth2-server-ktor + ${myndocs.oauth.version} + +``` + +## Implementation +```kotlin +embeddedServer(Netty, 8080) { + install(Oauth2ServerFeature) { + identityService = InMemoryIdentity() + .identity { + username = "foo" + password = "bar" + } + clientService = InMemoryClient() + .client { + clientId = "testapp" + clientSecret = "testpass" + scopes = setOf("trusted") + redirectUris = setOf("https://localhost:8080/callback") + authorizedGrantTypes = setOf( + AuthorizedGrantType.AUTHORIZATION_CODE, + AuthorizedGrantType.PASSWORD, + AuthorizedGrantType.IMPLICIT, + AuthorizedGrantType.REFRESH_TOKEN + ) + } + tokenStore = InMemoryTokenStore() + } +}.start(wait = true) +``` \ No newline at end of file diff --git a/docs/sparkjava.md b/docs/sparkjava.md new file mode 100644 index 0000000..f4f8d57 --- /dev/null +++ b/docs/sparkjava.md @@ -0,0 +1,35 @@ +# Spark java + +## Dependencies +```xml + + nl.myndocs + oauth2-server-sparkjava + ${myndocs.oauth.version} + +``` + +## Implementation +```kotlin +Oauth2Server.configureOauth2Server { + identityService = InMemoryIdentity() + .identity { + username = "foo" + password = "bar" + } + clientService = InMemoryClient() + .client { + clientId = "testapp" + clientSecret = "testpass" + scopes = setOf("trusted") + redirectUris = setOf("https://localhost:4567/callback") + authorizedGrantTypes = setOf( + AuthorizedGrantType.AUTHORIZATION_CODE, + AuthorizedGrantType.PASSWORD, + AuthorizedGrantType.IMPLICIT, + AuthorizedGrantType.REFRESH_TOKEN + ) + } + tokenStore = InMemoryTokenStore() +} +``` diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt index a0e526e..fdc86ff 100644 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt @@ -4,6 +4,8 @@ import nl.myndocs.oauth2.authenticator.Authorizer import nl.myndocs.oauth2.exception.* import nl.myndocs.oauth2.grant.Granter import nl.myndocs.oauth2.grant.GrantingCall +import nl.myndocs.oauth2.grant.redirect +import nl.myndocs.oauth2.grant.tokenInfo import nl.myndocs.oauth2.identity.TokenInfo import nl.myndocs.oauth2.request.CallContext import nl.myndocs.oauth2.request.RedirectAuthorizationCodeRequest @@ -11,12 +13,12 @@ import nl.myndocs.oauth2.request.RedirectTokenRequest import nl.myndocs.oauth2.request.headerCaseInsensitive class CallRouter( - private val tokenService: TokenService, val tokenEndpoint: String, val authorizeEndpoint: String, val tokenInfoEndpoint: String, private val tokenInfoCallback: (TokenInfo) -> Map, - private val granters: List Granter> + private val granters: List Granter>, + private val grantingCallFactory: (CallContext) -> GrantingCall ) { companion object { const val METHOD_POST = "post" @@ -46,12 +48,7 @@ class CallRouter( val grantType = callContext.formParameters["grant_type"] ?: throw InvalidRequestException("'grant_type' not given") - val grantingCall = object: GrantingCall { - override val callContext: CallContext - get() = callContext - - override val tokenService = this@CallRouter.tokenService - } + val grantingCall = grantingCallFactory(callContext) val granterMap = granters .map { @@ -75,13 +72,12 @@ class CallRouter( fun routeAuthorizationCodeRedirect( callContext: CallContext, - tokenService: TokenService, authorizer: Authorizer ) { val queryParameters = callContext.queryParameters val credentials = authorizer.extractCredentials() try { - val redirect = tokenService.redirect( + val redirect = grantingCallFactory(callContext).redirect( RedirectAuthorizationCodeRequest( queryParameters["client_id"], queryParameters["redirect_uri"], @@ -109,14 +105,13 @@ class CallRouter( fun routeAccessTokenRedirect( callContext: CallContext, - tokenService: TokenService, authorizer: Authorizer ) { val queryParameters = callContext.queryParameters val credentials = authorizer.extractCredentials() try { - val redirect = tokenService.redirect( + val redirect = grantingCallFactory(callContext).redirect( RedirectTokenRequest( queryParameters["client_id"], queryParameters["redirect_uri"], @@ -160,8 +155,8 @@ class CallRouter( } when (responseType) { - "code" -> routeAuthorizationCodeRedirect(callContext, tokenService, authorizer) - "token" -> routeAccessTokenRedirect(callContext, tokenService, authorizer) + "code" -> routeAuthorizationCodeRedirect(callContext, authorizer) + "token" -> routeAccessTokenRedirect(callContext, authorizer) } } catch (oauthException: OauthException) { callContext.respondStatus(STATUS_BAD_REQUEST) @@ -183,7 +178,7 @@ class CallRouter( val token = authorization.substring(7) - val tokenInfoCallback = tokenInfoCallback(tokenService.tokenInfo(token)) + val tokenInfoCallback = tokenInfoCallback(grantingCallFactory(callContext).tokenInfo(token)) callContext.respondJson(tokenInfoCallback) } diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/Oauth2TokenService.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/Oauth2TokenService.kt deleted file mode 100644 index 7ccaca3..0000000 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/Oauth2TokenService.kt +++ /dev/null @@ -1,354 +0,0 @@ -package nl.myndocs.oauth2 - -import nl.myndocs.oauth2.authenticator.Authenticator -import nl.myndocs.oauth2.authenticator.IdentityScopeVerifier -import nl.myndocs.oauth2.client.AuthorizedGrantType -import nl.myndocs.oauth2.client.Client -import nl.myndocs.oauth2.client.ClientService -import nl.myndocs.oauth2.exception.* -import nl.myndocs.oauth2.identity.Identity -import nl.myndocs.oauth2.identity.IdentityService -import nl.myndocs.oauth2.identity.TokenInfo -import nl.myndocs.oauth2.request.* -import nl.myndocs.oauth2.response.TokenResponse -import nl.myndocs.oauth2.scope.ScopeParser -import nl.myndocs.oauth2.token.AccessToken -import nl.myndocs.oauth2.token.CodeToken -import nl.myndocs.oauth2.token.TokenStore -import nl.myndocs.oauth2.token.converter.AccessTokenConverter -import nl.myndocs.oauth2.token.converter.CodeTokenConverter -import nl.myndocs.oauth2.token.converter.RefreshTokenConverter - -class Oauth2TokenService( - private val identityService: IdentityService, - private val clientService: ClientService, - private val tokenStore: TokenStore, - private val accessTokenConverter: AccessTokenConverter, - private val refreshTokenConverter: RefreshTokenConverter, - private val codeTokenConverter: CodeTokenConverter -) : TokenService { - private val INVALID_REQUEST_FIELD_MESSAGE = "'%s' field is missing" - /** - * @throws InvalidIdentityException - * @throws InvalidClientException - * @throws InvalidScopeException - */ - override fun authorize(passwordGrantRequest: PasswordGrantRequest): TokenResponse { - throwExceptionIfUnverifiedClient(passwordGrantRequest) - - if (passwordGrantRequest.username == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("username")) - } - - if (passwordGrantRequest.password == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("password")) - } - - val requestedClient = clientService.clientOf(passwordGrantRequest.clientId!!) ?: throw InvalidClientException() - - val authorizedGrantType = AuthorizedGrantType.PASSWORD - if (!requestedClient.authorizedGrantTypes.contains(authorizedGrantType)) { - throw InvalidGrantException("Authorize not allowed: '$authorizedGrantType'") - } - - val requestedIdentity = identityService.identityOf( - requestedClient, passwordGrantRequest.username - ) - - if (requestedIdentity == null || !identityService.validCredentials(requestedClient, requestedIdentity, passwordGrantRequest.password)) { - throw InvalidIdentityException() - } - - var requestedScopes = ScopeParser.parseScopes(passwordGrantRequest.scope) - .toSet() - - if (passwordGrantRequest.scope == null) { - requestedScopes = requestedClient.clientScopes - } - - validateScopes(requestedClient, requestedIdentity, requestedScopes) - - val accessToken = accessTokenConverter.convertToToken( - requestedIdentity.username, - requestedClient.clientId, - requestedScopes, - refreshTokenConverter.convertToToken( - requestedIdentity.username, - requestedClient.clientId, - requestedScopes - ) - ) - - tokenStore.storeAccessToken(accessToken) - - return accessToken.toTokenResponse() - } - - override fun authorize(authorizationCodeRequest: AuthorizationCodeRequest): TokenResponse { - throwExceptionIfUnverifiedClient(authorizationCodeRequest) - - if (authorizationCodeRequest.code == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("code")) - } - - if (authorizationCodeRequest.redirectUri == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("redirect_uri")) - } - - val consumeCodeToken = tokenStore.consumeCodeToken(authorizationCodeRequest.code) - ?: throw InvalidGrantException() - - - if (consumeCodeToken.redirectUri != authorizationCodeRequest.redirectUri || consumeCodeToken.clientId != authorizationCodeRequest.clientId) { - throw InvalidGrantException() - } - - val accessToken = accessTokenConverter.convertToToken( - consumeCodeToken.username, - consumeCodeToken.clientId, - consumeCodeToken.scopes, - refreshTokenConverter.convertToToken( - consumeCodeToken.username, - consumeCodeToken.clientId, - consumeCodeToken.scopes - ) - ) - - tokenStore.storeAccessToken(accessToken) - - return accessToken.toTokenResponse() - } - - override fun authorize(clientCredentialsRequest: ClientCredentialsRequest): TokenResponse { - throwExceptionIfUnverifiedClient(clientCredentialsRequest) - - val requestedClient = clientService.clientOf(clientCredentialsRequest.clientId!!) ?: throw InvalidClientException() - - val scopes = clientCredentialsRequest.scope - ?.let { ScopeParser.parseScopes(it).toSet() } - ?: requestedClient.clientScopes - - val accessToken = accessTokenConverter.convertToToken( - username = null, - clientId = clientCredentialsRequest.clientId, - requestedScopes = scopes, - refreshToken = refreshTokenConverter.convertToToken( - username = null, - clientId = clientCredentialsRequest.clientId, - requestedScopes = scopes - ) - ) - - tokenStore.storeAccessToken(accessToken) - - return accessToken.toTokenResponse() - } - - override fun refresh(refreshTokenRequest: RefreshTokenRequest): TokenResponse { - throwExceptionIfUnverifiedClient(refreshTokenRequest) - - if (refreshTokenRequest.refreshToken == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("refresh_token")) - } - - val refreshToken = tokenStore.refreshToken(refreshTokenRequest.refreshToken) ?: throw InvalidGrantException() - - if (refreshToken.clientId != refreshTokenRequest.clientId) { - throw InvalidGrantException() - } - - val client = clientService.clientOf(refreshToken.clientId) ?: throw InvalidClientException() - - val authorizedGrantType = AuthorizedGrantType.REFRESH_TOKEN - if (!client.authorizedGrantTypes.contains(authorizedGrantType)) { - throw InvalidGrantException("Authorize not allowed: '$authorizedGrantType'") - } - - val accessToken = accessTokenConverter.convertToToken( - refreshToken.username, - refreshToken.clientId, - refreshToken.scopes, - refreshTokenConverter.convertToToken(refreshToken) - ) - - tokenStore.storeAccessToken(accessToken) - - return accessToken.toTokenResponse() - } - - override fun redirect( - redirect: RedirectAuthorizationCodeRequest, - authenticator: Authenticator?, - identityScopeVerifier: IdentityScopeVerifier? - ): CodeToken { - if (redirect.clientId == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("client_id")) - } - - if (redirect.username == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("username")) - } - - if (redirect.password == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("password")) - } - if (redirect.redirectUri == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("redirect_uri")) - } - - val clientOf = clientService.clientOf(redirect.clientId) ?: throw InvalidClientException() - - if (!clientOf.redirectUris.contains(redirect.redirectUri)) { - throw InvalidGrantException("invalid 'redirect_uri'") - } - - val authorizedGrantType = AuthorizedGrantType.AUTHORIZATION_CODE - if (!clientOf.authorizedGrantTypes.contains(authorizedGrantType)) { - throw InvalidGrantException("Authorize not allowed: '$authorizedGrantType'") - } - - val identityOf = identityService.identityOf(clientOf, redirect.username) ?: throw InvalidIdentityException() - - var validIdentity = authenticator?.validCredentials(clientOf, identityOf, redirect.password) - ?: identityService.validCredentials(clientOf, identityOf, redirect.password) - - if (!validIdentity) { - throw InvalidIdentityException() - } - - var requestedScopes = ScopeParser.parseScopes(redirect.scope) - - if (redirect.scope == null) { - requestedScopes = clientOf.clientScopes - } - - validateScopes(clientOf, identityOf, requestedScopes, identityScopeVerifier) - - val codeToken = codeTokenConverter.convertToToken( - identityOf.username, - clientOf.clientId, - redirect.redirectUri, - requestedScopes - ) - - tokenStore.storeCodeToken(codeToken) - - return codeToken - } - - override fun redirect( - redirect: RedirectTokenRequest, - authenticator: Authenticator?, - identityScopeVerifier: IdentityScopeVerifier? - ): AccessToken { - if (redirect.clientId == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("client_id")) - } - - if (redirect.username == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("username")) - } - - if (redirect.password == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("password")) - } - if (redirect.redirectUri == null) { - throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("redirect_uri")) - } - - val clientOf = clientService.clientOf(redirect.clientId) ?: throw InvalidClientException() - - if (!clientOf.redirectUris.contains(redirect.redirectUri)) { - throw InvalidGrantException("invalid 'redirect_uri'") - } - - val authorizedGrantType = AuthorizedGrantType.IMPLICIT - if (!clientOf.authorizedGrantTypes.contains(authorizedGrantType)) { - throw InvalidGrantException("Authorize not allowed: '$authorizedGrantType'") - } - - val identityOf = identityService.identityOf(clientOf, redirect.username) ?: throw InvalidIdentityException() - - var validIdentity = authenticator?.validCredentials(clientOf, identityOf, redirect.password) - ?: identityService.validCredentials(clientOf, identityOf, redirect.password) - - if (!validIdentity) { - throw InvalidIdentityException() - } - - var requestedScopes = ScopeParser.parseScopes(redirect.scope) - - if (redirect.scope == null) { - requestedScopes = clientOf.clientScopes - } - - validateScopes(clientOf, identityOf, requestedScopes, identityScopeVerifier) - - val accessToken = accessTokenConverter.convertToToken( - identityOf.username, - clientOf.clientId, - requestedScopes, - null - ) - - tokenStore.storeAccessToken(accessToken) - - return accessToken - } - - private fun validateScopes( - client: Client, - identity: Identity, - requestedScopes: Set, - identityScopeVerifier: IdentityScopeVerifier? = null) { - 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 ivalidScopes = requestedScopes.minus(allowedScopes) - if (ivalidScopes.isNotEmpty()) { - throw InvalidScopeException(ivalidScopes) - } - } - - override fun tokenInfo(accessToken: String): TokenInfo { - val storedAccessToken = tokenStore.accessToken(accessToken) ?: throw InvalidGrantException() - val client = clientService.clientOf(storedAccessToken.clientId) ?: throw InvalidClientException() - val identity = storedAccessToken.username?.let { identityService.identityOf(client, it) } - - return TokenInfo( - identity, - client, - storedAccessToken.scopes - ) - } - - private fun throwExceptionIfUnverifiedClient(clientRequest: ClientRequest) { - val clientId = clientRequest.clientId - ?: throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("client_id")) - - val clientSecret = clientRequest.clientSecret - ?: throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("client_secret")) - - val client = clientService.clientOf(clientId) ?: throw InvalidClientException() - - if (!clientService.validClient(client, clientSecret)) { - throw InvalidClientException() - } - } - - private fun scopesAllowed(clientScopes: Set, requestedScopes: Set): Boolean { - return clientScopes.containsAll(requestedScopes) - } - - private fun AccessToken.toTokenResponse() = TokenResponse( - accessToken, - tokenType, - expiresIn(), - refreshToken?.refreshToken - ) -} \ No newline at end of file diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/TokenService.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/TokenService.kt deleted file mode 100644 index 215405e..0000000 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/TokenService.kt +++ /dev/null @@ -1,33 +0,0 @@ -package nl.myndocs.oauth2 - -import nl.myndocs.oauth2.authenticator.Authenticator -import nl.myndocs.oauth2.authenticator.IdentityScopeVerifier -import nl.myndocs.oauth2.identity.TokenInfo -import nl.myndocs.oauth2.request.* -import nl.myndocs.oauth2.response.TokenResponse -import nl.myndocs.oauth2.token.AccessToken -import nl.myndocs.oauth2.token.CodeToken - -interface TokenService { - fun authorize(passwordGrantRequest: PasswordGrantRequest): TokenResponse - - fun authorize(authorizationCodeRequest: AuthorizationCodeRequest): TokenResponse - - fun authorize(clientCredentialsRequest: ClientCredentialsRequest): TokenResponse - - fun refresh(refreshTokenRequest: RefreshTokenRequest): TokenResponse - - fun redirect( - redirect: RedirectAuthorizationCodeRequest, - authenticator: Authenticator?, - identityScopeVerifier: IdentityScopeVerifier? - ): CodeToken - - fun redirect( - redirect: RedirectTokenRequest, - authenticator: Authenticator?, - identityScopeVerifier: IdentityScopeVerifier? - ): AccessToken - - fun tokenInfo(accessToken: String): TokenInfo -} \ No newline at end of file diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/CallRouterBuilder.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/CallRouterBuilder.kt index 710ef13..89eca27 100644 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/CallRouterBuilder.kt +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/CallRouterBuilder.kt @@ -1,8 +1,9 @@ package nl.myndocs.oauth2.config -import nl.myndocs.oauth2.* +import nl.myndocs.oauth2.CallRouter import nl.myndocs.oauth2.grant.* import nl.myndocs.oauth2.identity.TokenInfo +import nl.myndocs.oauth2.request.CallContext internal object CallRouterBuilder { class Configuration { @@ -15,19 +16,10 @@ internal object CallRouterBuilder { "scopes" to tokenInfo.scopes ).filterValues { it != null } } - var tokenService: TokenService? = null var granters: List Granter> = listOf() } - fun build(configurer: Configuration.() -> Unit): CallRouter { - val configuration = Configuration() - configurer(configuration) - - return build(configuration) - } - - fun build(configuration: Configuration) = CallRouter( - configuration.tokenService!!, + fun build(configuration: Configuration, grantingCallFactory: (CallContext) -> GrantingCall) = CallRouter( configuration.tokenEndpoint, configuration.authorizeEndpoint, configuration.tokenInfoEndpoint, @@ -37,6 +29,7 @@ internal object CallRouterBuilder { { grantAuthorizationCode() }, { grantClientCredentials() }, { grantRefreshToken() } - ) + configuration.granters + ) + configuration.granters, + grantingCallFactory ) } \ No newline at end of file diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/Configuration.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/Configuration.kt index 90b8a19..67e4044 100644 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/Configuration.kt +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/Configuration.kt @@ -1,12 +1,10 @@ package nl.myndocs.oauth2.config import nl.myndocs.oauth2.CallRouter -import nl.myndocs.oauth2.TokenService import nl.myndocs.oauth2.authenticator.Authorizer import nl.myndocs.oauth2.request.CallContext data class Configuration( - val tokenService: TokenService, val callRouter: CallRouter, val authorizerFactory: (CallContext) -> Authorizer ) \ No newline at end of file diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/ConfigurationBuilder.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/ConfigurationBuilder.kt index 0f69b0e..eee7012 100644 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/ConfigurationBuilder.kt +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/ConfigurationBuilder.kt @@ -1,23 +1,20 @@ package nl.myndocs.oauth2.config -import nl.myndocs.oauth2.TokenService 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.token.TokenStore +import nl.myndocs.oauth2.token.converter.* object ConfigurationBuilder { class Configuration { internal val callRouterConfiguration = CallRouterBuilder.Configuration() - var tokenService: TokenService? - get() = callRouterConfiguration.tokenService - set(value) { - callRouterConfiguration.tokenService = value - } - var authorizationEndpoint: String get() = callRouterConfiguration.authorizeEndpoint set(value) { @@ -49,15 +46,34 @@ object ConfigurationBuilder { } var authorizerFactory: (CallContext) -> Authorizer = ::BasicAuthorizer + + var identityService: IdentityService? = null + var clientService: ClientService? = null + var tokenStore: TokenStore? = null + var accessTokenConverter: AccessTokenConverter = UUIDAccessTokenConverter() + var refreshTokenConverter: RefreshTokenConverter = UUIDRefreshTokenConverter() + var codeTokenConverter: CodeTokenConverter = UUIDCodeTokenConverter() } fun build(configurer: Configuration.() -> Unit): nl.myndocs.oauth2.config.Configuration { val configuration = Configuration() configurer(configuration) + val grantingCallFactory: (CallContext) -> GrantingCall = { callContext -> + object : GrantingCall { + override val callContext = callContext + override val identityService = configuration.identityService!! + override val clientService = configuration.clientService!! + override val tokenStore = configuration.tokenStore!! + override val converters = Converters( + configuration.accessTokenConverter, + configuration.refreshTokenConverter, + configuration.codeTokenConverter + ) + } + } return nl.myndocs.oauth2.config.Configuration( - configuration.tokenService!!, - CallRouterBuilder.build(configuration.callRouterConfiguration), + CallRouterBuilder.build(configuration.callRouterConfiguration, grantingCallFactory), configuration.authorizerFactory ) } diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/Oauth2TokenServiceBuilder.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/Oauth2TokenServiceBuilder.kt deleted file mode 100644 index 7fbc9a3..0000000 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/Oauth2TokenServiceBuilder.kt +++ /dev/null @@ -1,32 +0,0 @@ -package nl.myndocs.oauth2.config - -import nl.myndocs.oauth2.Oauth2TokenService -import nl.myndocs.oauth2.client.ClientService -import nl.myndocs.oauth2.identity.IdentityService -import nl.myndocs.oauth2.token.TokenStore -import nl.myndocs.oauth2.token.converter.* - -object Oauth2TokenServiceBuilder { - class Configuration { - var identityService: IdentityService? = null - var clientService: ClientService? = null - var tokenStore: TokenStore? = null - var accessTokenConverter: AccessTokenConverter = UUIDAccessTokenConverter() - var refreshTokenConverter: RefreshTokenConverter = UUIDRefreshTokenConverter() - var codeTokenConverter: CodeTokenConverter = UUIDCodeTokenConverter() - } - - fun build(configurer: Configuration.() -> Unit): Oauth2TokenService { - val configuration = Configuration() - configurer(configuration) - - return Oauth2TokenService( - configuration.identityService!!, - configuration.clientService!!, - configuration.tokenStore!!, - configuration.accessTokenConverter, - configuration.refreshTokenConverter, - configuration.codeTokenConverter - ) - } -} \ No newline at end of file diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterAuthorize.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterAuthorize.kt new file mode 100644 index 0000000..a3c873e --- /dev/null +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterAuthorize.kt @@ -0,0 +1,126 @@ +package nl.myndocs.oauth2.grant + +import nl.myndocs.oauth2.client.AuthorizedGrantType +import nl.myndocs.oauth2.exception.* +import nl.myndocs.oauth2.request.AuthorizationCodeRequest +import nl.myndocs.oauth2.request.ClientCredentialsRequest +import nl.myndocs.oauth2.request.PasswordGrantRequest +import nl.myndocs.oauth2.response.TokenResponse +import nl.myndocs.oauth2.scope.ScopeParser + + +/** + * @throws InvalidIdentityException + * @throws InvalidClientException + * @throws InvalidScopeException + */ +fun GrantingCall.authorize(passwordGrantRequest: PasswordGrantRequest): TokenResponse { + throwExceptionIfUnverifiedClient(passwordGrantRequest) + + if (passwordGrantRequest.username == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("username")) + } + + if (passwordGrantRequest.password == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("password")) + } + + val requestedClient = clientService.clientOf(passwordGrantRequest.clientId!!) ?: throw InvalidClientException() + + val authorizedGrantType = AuthorizedGrantType.PASSWORD + if (!requestedClient.authorizedGrantTypes.contains(authorizedGrantType)) { + throw InvalidGrantException("Authorize not allowed: '$authorizedGrantType'") + } + + val requestedIdentity = identityService.identityOf( + requestedClient, passwordGrantRequest.username + ) + + if (requestedIdentity == null || !identityService.validCredentials(requestedClient, requestedIdentity, passwordGrantRequest.password)) { + throw InvalidIdentityException() + } + + var requestedScopes = ScopeParser.parseScopes(passwordGrantRequest.scope) + .toSet() + + if (passwordGrantRequest.scope == null) { + requestedScopes = requestedClient.clientScopes + } + + validateScopes(requestedClient, requestedIdentity, requestedScopes) + + val accessToken = converters.accessTokenConverter.convertToToken( + requestedIdentity.username, + requestedClient.clientId, + requestedScopes, + converters.refreshTokenConverter.convertToToken( + requestedIdentity.username, + requestedClient.clientId, + requestedScopes + ) + ) + + tokenStore.storeAccessToken(accessToken) + + return accessToken.toTokenResponse() +} + +fun GrantingCall.authorize(authorizationCodeRequest: AuthorizationCodeRequest): TokenResponse { + throwExceptionIfUnverifiedClient(authorizationCodeRequest) + + if (authorizationCodeRequest.code == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("code")) + } + + if (authorizationCodeRequest.redirectUri == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("redirect_uri")) + } + + val consumeCodeToken = tokenStore.consumeCodeToken(authorizationCodeRequest.code) + ?: throw InvalidGrantException() + + + if (consumeCodeToken.redirectUri != authorizationCodeRequest.redirectUri || consumeCodeToken.clientId != authorizationCodeRequest.clientId) { + throw InvalidGrantException() + } + + val accessToken = converters.accessTokenConverter.convertToToken( + consumeCodeToken.username, + consumeCodeToken.clientId, + consumeCodeToken.scopes, + converters.refreshTokenConverter.convertToToken( + consumeCodeToken.username, + consumeCodeToken.clientId, + consumeCodeToken.scopes + ) + ) + + tokenStore.storeAccessToken(accessToken) + + return accessToken.toTokenResponse() +} + +fun GrantingCall.authorize(clientCredentialsRequest: ClientCredentialsRequest): TokenResponse { + throwExceptionIfUnverifiedClient(clientCredentialsRequest) + + val requestedClient = clientService.clientOf(clientCredentialsRequest.clientId!!) ?: throw InvalidClientException() + + val scopes = clientCredentialsRequest.scope + ?.let { ScopeParser.parseScopes(it).toSet() } + ?: requestedClient.clientScopes + + val accessToken = converters.accessTokenConverter.convertToToken( + username = null, + clientId = clientCredentialsRequest.clientId, + requestedScopes = scopes, + refreshToken = converters.refreshTokenConverter.convertToToken( + username = null, + clientId = clientCredentialsRequest.clientId, + requestedScopes = scopes + ) + ) + + tokenStore.storeAccessToken(accessToken) + + return accessToken.toTokenResponse() +} diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterDefault.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterDefault.kt index d3ad54c..f2bbe5a 100644 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterDefault.kt +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterDefault.kt @@ -1,10 +1,20 @@ package nl.myndocs.oauth2.grant +import nl.myndocs.oauth2.authenticator.IdentityScopeVerifier +import nl.myndocs.oauth2.client.Client +import nl.myndocs.oauth2.exception.InvalidClientException +import nl.myndocs.oauth2.exception.InvalidGrantException +import nl.myndocs.oauth2.exception.InvalidRequestException +import nl.myndocs.oauth2.exception.InvalidScopeException +import nl.myndocs.oauth2.identity.Identity +import nl.myndocs.oauth2.identity.TokenInfo import nl.myndocs.oauth2.request.* +import nl.myndocs.oauth2.response.TokenResponse +import nl.myndocs.oauth2.token.AccessToken import nl.myndocs.oauth2.token.toMap fun GrantingCall.grantPassword() = granter("password") { - val tokenResponse = tokenService.authorize( + val tokenResponse = authorize( PasswordGrantRequest( callContext.formParameters["client_id"], callContext.formParameters["client_secret"], @@ -18,7 +28,7 @@ fun GrantingCall.grantPassword() = granter("password") { } fun GrantingCall.grantClientCredentials() = granter("client_credentials") { - val tokenResponse = tokenService.authorize(ClientCredentialsRequest( + val tokenResponse = authorize(ClientCredentialsRequest( callContext.formParameters["client_id"], callContext.formParameters["client_secret"], callContext.formParameters["scope"] @@ -28,7 +38,7 @@ fun GrantingCall.grantClientCredentials() = granter("client_credentials") { } fun GrantingCall.grantRefreshToken() = granter("refresh_token") { - val accessToken = tokenService.refresh( + val accessToken = refresh( RefreshTokenRequest( callContext.formParameters["client_id"], callContext.formParameters["client_secret"], @@ -40,7 +50,7 @@ fun GrantingCall.grantRefreshToken() = granter("refresh_token") { } fun GrantingCall.grantAuthorizationCode() = granter("authorization_code") { - val accessToken = tokenService.authorize( + val accessToken = authorize( AuthorizationCodeRequest( callContext.formParameters["client_id"], callContext.formParameters["client_secret"], @@ -51,3 +61,61 @@ fun GrantingCall.grantAuthorizationCode() = granter("authorization_code") { callContext.respondJson(accessToken.toMap()) } + +internal val INVALID_REQUEST_FIELD_MESSAGE = "'%s' field is missing" + +fun GrantingCall.validateScopes( + client: Client, + identity: Identity, + requestedScopes: Set, + identityScopeVerifier: IdentityScopeVerifier? = null) { + 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 ivalidScopes = requestedScopes.minus(allowedScopes) + if (ivalidScopes.isNotEmpty()) { + throw InvalidScopeException(ivalidScopes) + } +} + +fun GrantingCall.tokenInfo(accessToken: String): TokenInfo { + val storedAccessToken = tokenStore.accessToken(accessToken) ?: throw InvalidGrantException() + val client = clientService.clientOf(storedAccessToken.clientId) ?: throw InvalidClientException() + val identity = storedAccessToken.username?.let { identityService.identityOf(client, it) } + + return TokenInfo( + identity, + client, + storedAccessToken.scopes + ) +} + +fun GrantingCall.throwExceptionIfUnverifiedClient(clientRequest: ClientRequest) { + val clientId = clientRequest.clientId + ?: throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("client_id")) + + val clientSecret = clientRequest.clientSecret + ?: throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("client_secret")) + + val client = clientService.clientOf(clientId) ?: throw InvalidClientException() + + if (!clientService.validClient(client, clientSecret)) { + throw InvalidClientException() + } +} + +fun GrantingCall.scopesAllowed(clientScopes: Set, requestedScopes: Set): Boolean { + return clientScopes.containsAll(requestedScopes) +} + +fun AccessToken.toTokenResponse() = TokenResponse( + accessToken, + tokenType, + expiresIn(), + refreshToken?.refreshToken +) \ No newline at end of file diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterRedirect.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterRedirect.kt new file mode 100644 index 0000000..e6f54f1 --- /dev/null +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterRedirect.kt @@ -0,0 +1,135 @@ +package nl.myndocs.oauth2.grant + +import nl.myndocs.oauth2.authenticator.Authenticator +import nl.myndocs.oauth2.authenticator.IdentityScopeVerifier +import nl.myndocs.oauth2.client.AuthorizedGrantType +import nl.myndocs.oauth2.exception.InvalidClientException +import nl.myndocs.oauth2.exception.InvalidGrantException +import nl.myndocs.oauth2.exception.InvalidIdentityException +import nl.myndocs.oauth2.exception.InvalidRequestException +import nl.myndocs.oauth2.request.RedirectAuthorizationCodeRequest +import nl.myndocs.oauth2.request.RedirectTokenRequest +import nl.myndocs.oauth2.scope.ScopeParser +import nl.myndocs.oauth2.token.AccessToken +import nl.myndocs.oauth2.token.CodeToken + + +fun GrantingCall.redirect( + redirect: RedirectAuthorizationCodeRequest, + authenticator: Authenticator?, + identityScopeVerifier: IdentityScopeVerifier? +): CodeToken { + if (redirect.clientId == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("client_id")) + } + + if (redirect.username == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("username")) + } + + if (redirect.password == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("password")) + } + if (redirect.redirectUri == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("redirect_uri")) + } + + val clientOf = clientService.clientOf(redirect.clientId) ?: throw InvalidClientException() + + if (!clientOf.redirectUris.contains(redirect.redirectUri)) { + throw InvalidGrantException("invalid 'redirect_uri'") + } + + val authorizedGrantType = AuthorizedGrantType.AUTHORIZATION_CODE + if (!clientOf.authorizedGrantTypes.contains(authorizedGrantType)) { + throw InvalidGrantException("Authorize not allowed: '$authorizedGrantType'") + } + + val identityOf = identityService.identityOf(clientOf, redirect.username) ?: throw InvalidIdentityException() + + var validIdentity = authenticator?.validCredentials(clientOf, identityOf, redirect.password) + ?: identityService.validCredentials(clientOf, identityOf, redirect.password) + + if (!validIdentity) { + throw InvalidIdentityException() + } + + var requestedScopes = ScopeParser.parseScopes(redirect.scope) + + if (redirect.scope == null) { + requestedScopes = clientOf.clientScopes + } + + validateScopes(clientOf, identityOf, requestedScopes, identityScopeVerifier) + + val codeToken = converters.codeTokenConverter.convertToToken( + identityOf.username, + clientOf.clientId, + redirect.redirectUri, + requestedScopes + ) + + tokenStore.storeCodeToken(codeToken) + + return codeToken +} + +fun GrantingCall.redirect( + redirect: RedirectTokenRequest, + authenticator: Authenticator?, + identityScopeVerifier: IdentityScopeVerifier? +): AccessToken { + if (redirect.clientId == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("client_id")) + } + + if (redirect.username == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("username")) + } + + if (redirect.password == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("password")) + } + if (redirect.redirectUri == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("redirect_uri")) + } + + val clientOf = clientService.clientOf(redirect.clientId) ?: throw InvalidClientException() + + if (!clientOf.redirectUris.contains(redirect.redirectUri)) { + throw InvalidGrantException("invalid 'redirect_uri'") + } + + val authorizedGrantType = AuthorizedGrantType.IMPLICIT + if (!clientOf.authorizedGrantTypes.contains(authorizedGrantType)) { + throw InvalidGrantException("Authorize not allowed: '$authorizedGrantType'") + } + + val identityOf = identityService.identityOf(clientOf, redirect.username) ?: throw InvalidIdentityException() + + var validIdentity = authenticator?.validCredentials(clientOf, identityOf, redirect.password) + ?: identityService.validCredentials(clientOf, identityOf, redirect.password) + + if (!validIdentity) { + throw InvalidIdentityException() + } + + var requestedScopes = ScopeParser.parseScopes(redirect.scope) + + if (redirect.scope == null) { + requestedScopes = clientOf.clientScopes + } + + validateScopes(clientOf, identityOf, requestedScopes, identityScopeVerifier) + + val accessToken = converters.accessTokenConverter.convertToToken( + identityOf.username, + clientOf.clientId, + requestedScopes, + null + ) + + tokenStore.storeAccessToken(accessToken) + + return accessToken +} diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterRefresh.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterRefresh.kt new file mode 100644 index 0000000..5c4c456 --- /dev/null +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterRefresh.kt @@ -0,0 +1,41 @@ +package nl.myndocs.oauth2.grant + +import nl.myndocs.oauth2.client.AuthorizedGrantType +import nl.myndocs.oauth2.exception.InvalidClientException +import nl.myndocs.oauth2.exception.InvalidGrantException +import nl.myndocs.oauth2.exception.InvalidRequestException +import nl.myndocs.oauth2.request.RefreshTokenRequest +import nl.myndocs.oauth2.response.TokenResponse + + +fun GrantingCall.refresh(refreshTokenRequest: RefreshTokenRequest): TokenResponse { + throwExceptionIfUnverifiedClient(refreshTokenRequest) + + if (refreshTokenRequest.refreshToken == null) { + throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("refresh_token")) + } + + val refreshToken = tokenStore.refreshToken(refreshTokenRequest.refreshToken) ?: throw InvalidGrantException() + + if (refreshToken.clientId != refreshTokenRequest.clientId) { + throw InvalidGrantException() + } + + val client = clientService.clientOf(refreshToken.clientId) ?: throw InvalidClientException() + + val authorizedGrantType = AuthorizedGrantType.REFRESH_TOKEN + if (!client.authorizedGrantTypes.contains(authorizedGrantType)) { + throw InvalidGrantException("Authorize not allowed: '$authorizedGrantType'") + } + + val accessToken = converters.accessTokenConverter.convertToToken( + refreshToken.username, + refreshToken.clientId, + refreshToken.scopes, + converters.refreshTokenConverter.convertToToken(refreshToken) + ) + + tokenStore.storeAccessToken(accessToken) + + return accessToken.toTokenResponse() +} diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantingCall.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantingCall.kt index 046d1d5..b219253 100644 --- a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantingCall.kt +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantingCall.kt @@ -1,9 +1,15 @@ package nl.myndocs.oauth2.grant -import nl.myndocs.oauth2.TokenService +import nl.myndocs.oauth2.client.ClientService +import nl.myndocs.oauth2.identity.IdentityService import nl.myndocs.oauth2.request.CallContext +import nl.myndocs.oauth2.token.TokenStore +import nl.myndocs.oauth2.token.converter.Converters interface GrantingCall { val callContext: CallContext - val tokenService: TokenService + val identityService: IdentityService + val clientService: ClientService + val tokenStore: TokenStore + val converters: Converters } \ No newline at end of file diff --git a/oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/converter/Converters.kt b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/converter/Converters.kt new file mode 100644 index 0000000..8e40c27 --- /dev/null +++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/converter/Converters.kt @@ -0,0 +1,7 @@ +package nl.myndocs.oauth2.token.converter + +data class Converters( + val accessTokenConverter: AccessTokenConverter, + val refreshTokenConverter: RefreshTokenConverter, + val codeTokenConverter: CodeTokenConverter +) \ No newline at end of file diff --git a/oauth2-server-core/src/test/java/nl/myndocs/oauth2/AuthorizationCodeGrantTokenServiceTest.kt b/oauth2-server-core/src/test/java/nl/myndocs/oauth2/AuthorizationCodeGrantTokenServiceTest.kt index ef268e0..dc5cb3f 100644 --- a/oauth2-server-core/src/test/java/nl/myndocs/oauth2/AuthorizationCodeGrantTokenServiceTest.kt +++ b/oauth2-server-core/src/test/java/nl/myndocs/oauth2/AuthorizationCodeGrantTokenServiceTest.kt @@ -1,7 +1,6 @@ package nl.myndocs.oauth2 import io.mockk.every -import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension @@ -11,23 +10,30 @@ import nl.myndocs.oauth2.client.ClientService import nl.myndocs.oauth2.exception.InvalidClientException import nl.myndocs.oauth2.exception.InvalidGrantException import nl.myndocs.oauth2.exception.InvalidRequestException +import nl.myndocs.oauth2.grant.GrantingCall +import nl.myndocs.oauth2.grant.authorize import nl.myndocs.oauth2.identity.Identity import nl.myndocs.oauth2.identity.IdentityService import nl.myndocs.oauth2.request.AuthorizationCodeRequest +import nl.myndocs.oauth2.request.CallContext import nl.myndocs.oauth2.token.AccessToken import nl.myndocs.oauth2.token.CodeToken import nl.myndocs.oauth2.token.RefreshToken import nl.myndocs.oauth2.token.TokenStore import nl.myndocs.oauth2.token.converter.AccessTokenConverter import nl.myndocs.oauth2.token.converter.CodeTokenConverter +import nl.myndocs.oauth2.token.converter.Converters import nl.myndocs.oauth2.token.converter.RefreshTokenConverter import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.time.Instant @ExtendWith(MockKExtension::class) internal class AuthorizationCodeGrantTokenServiceTest { + @MockK + lateinit var callContext: CallContext @MockK lateinit var identityService: IdentityService @MockK @@ -41,8 +47,22 @@ internal class AuthorizationCodeGrantTokenServiceTest { @MockK lateinit var codeTokenConverter: CodeTokenConverter - @InjectMockKs - lateinit var tokenService: Oauth2TokenService + lateinit var grantingCall: GrantingCall + + @BeforeEach + fun initialize() { + grantingCall = object : GrantingCall { + override val callContext = this@AuthorizationCodeGrantTokenServiceTest.callContext + override val identityService = this@AuthorizationCodeGrantTokenServiceTest.identityService + override val clientService = this@AuthorizationCodeGrantTokenServiceTest.clientService + override val tokenStore = this@AuthorizationCodeGrantTokenServiceTest.tokenStore + override val converters = Converters( + this@AuthorizationCodeGrantTokenServiceTest.accessTokenConverter, + this@AuthorizationCodeGrantTokenServiceTest.refreshTokenConverter, + this@AuthorizationCodeGrantTokenServiceTest.codeTokenConverter + ) + } + } val clientId = "client-foo" val clientSecret = "client-bar" @@ -75,7 +95,7 @@ internal class AuthorizationCodeGrantTokenServiceTest { every { refreshTokenConverter.convertToToken(username, clientId, requestScopes) } returns refreshToken every { accessTokenConverter.convertToToken(username, clientId, requestScopes, refreshToken) } returns accessToken - tokenService.authorize(authorizationCodeRequest) + grantingCall.authorize(authorizationCodeRequest) } @Test @@ -84,7 +104,7 @@ internal class AuthorizationCodeGrantTokenServiceTest { assertThrows( InvalidClientException::class.java - ) { tokenService.authorize(authorizationCodeRequest) } + ) { grantingCall.authorize(authorizationCodeRequest) } } @Test @@ -95,7 +115,7 @@ internal class AuthorizationCodeGrantTokenServiceTest { assertThrows( InvalidClientException::class.java - ) { tokenService.authorize(authorizationCodeRequest) } + ) { grantingCall.authorize(authorizationCodeRequest) } } @Test @@ -113,7 +133,7 @@ internal class AuthorizationCodeGrantTokenServiceTest { assertThrows( InvalidRequestException::class.java - ) { tokenService.authorize(authorizationCodeRequest) } + ) { grantingCall.authorize(authorizationCodeRequest) } } @Test @@ -131,7 +151,7 @@ internal class AuthorizationCodeGrantTokenServiceTest { assertThrows( InvalidRequestException::class.java - ) { tokenService.authorize(authorizationCodeRequest) } + ) { grantingCall.authorize(authorizationCodeRequest) } } @Test @@ -153,7 +173,7 @@ internal class AuthorizationCodeGrantTokenServiceTest { assertThrows( InvalidGrantException::class.java - ) { tokenService.authorize(authorizationCodeRequest) } + ) { grantingCall.authorize(authorizationCodeRequest) } } @Test @@ -166,7 +186,7 @@ internal class AuthorizationCodeGrantTokenServiceTest { assertThrows( InvalidGrantException::class.java - ) { tokenService.authorize(authorizationCodeRequest) } + ) { grantingCall.authorize(authorizationCodeRequest) } } } \ No newline at end of file diff --git a/oauth2-server-core/src/test/java/nl/myndocs/oauth2/ClientCredentialsTokenServiceTest.kt b/oauth2-server-core/src/test/java/nl/myndocs/oauth2/ClientCredentialsTokenServiceTest.kt index a3c1277..4675402 100644 --- a/oauth2-server-core/src/test/java/nl/myndocs/oauth2/ClientCredentialsTokenServiceTest.kt +++ b/oauth2-server-core/src/test/java/nl/myndocs/oauth2/ClientCredentialsTokenServiceTest.kt @@ -1,7 +1,6 @@ package nl.myndocs.oauth2 import io.mockk.every -import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension @@ -10,21 +9,28 @@ import nl.myndocs.oauth2.client.AuthorizedGrantType import nl.myndocs.oauth2.client.Client import nl.myndocs.oauth2.client.ClientService import nl.myndocs.oauth2.exception.InvalidClientException +import nl.myndocs.oauth2.grant.GrantingCall +import nl.myndocs.oauth2.grant.authorize import nl.myndocs.oauth2.identity.IdentityService +import nl.myndocs.oauth2.request.CallContext import nl.myndocs.oauth2.request.ClientCredentialsRequest import nl.myndocs.oauth2.token.AccessToken import nl.myndocs.oauth2.token.RefreshToken import nl.myndocs.oauth2.token.TokenStore import nl.myndocs.oauth2.token.converter.AccessTokenConverter import nl.myndocs.oauth2.token.converter.CodeTokenConverter +import nl.myndocs.oauth2.token.converter.Converters import nl.myndocs.oauth2.token.converter.RefreshTokenConverter import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.time.Instant @ExtendWith(MockKExtension::class) internal class ClientCredentialsTokenServiceTest { + @MockK + lateinit var callContext: CallContext @MockK lateinit var identityService: IdentityService @MockK @@ -38,9 +44,22 @@ internal class ClientCredentialsTokenServiceTest { @MockK lateinit var codeTokenConverter: CodeTokenConverter - @InjectMockKs - lateinit var tokenService: Oauth2TokenService - + lateinit var grantingCall: GrantingCall + + @BeforeEach + fun initialize() { + grantingCall = object : GrantingCall { + override val callContext = this@ClientCredentialsTokenServiceTest.callContext + override val identityService = this@ClientCredentialsTokenServiceTest.identityService + override val clientService = this@ClientCredentialsTokenServiceTest.clientService + override val tokenStore = this@ClientCredentialsTokenServiceTest.tokenStore + override val converters = Converters( + this@ClientCredentialsTokenServiceTest.accessTokenConverter, + this@ClientCredentialsTokenServiceTest.refreshTokenConverter, + this@ClientCredentialsTokenServiceTest.codeTokenConverter + ) + } + } private val clientId = "client-foo" private val clientSecret = "client-secret" private val scope = "scope1" @@ -58,7 +77,7 @@ internal class ClientCredentialsTokenServiceTest { every { refreshTokenConverter.convertToToken(null, clientId, scopes) } returns refreshToken every { accessTokenConverter.convertToToken(null, clientId, scopes, refreshToken) } returns accessToken - tokenService.authorize(clientCredentialsRequest) + grantingCall.authorize(clientCredentialsRequest) verify { tokenStore.storeAccessToken(accessToken) } } @@ -69,7 +88,7 @@ internal class ClientCredentialsTokenServiceTest { Assertions.assertThrows( InvalidClientException::class.java - ) { tokenService.authorize(clientCredentialsRequest) } + ) { grantingCall.authorize(clientCredentialsRequest) } } @Test @@ -80,7 +99,7 @@ internal class ClientCredentialsTokenServiceTest { Assertions.assertThrows( InvalidClientException::class.java - ) { tokenService.authorize(clientCredentialsRequest) } + ) { grantingCall.authorize(clientCredentialsRequest) } } @Test @@ -101,6 +120,6 @@ internal class ClientCredentialsTokenServiceTest { every { refreshTokenConverter.convertToToken(null, clientId, requestScopes) } returns refreshToken every { accessTokenConverter.convertToToken(null, clientId, requestScopes, refreshToken) } returns accessToken - tokenService.authorize(clientCredentialsRequest) + grantingCall.authorize(clientCredentialsRequest) } } \ No newline at end of file diff --git a/oauth2-server-core/src/test/java/nl/myndocs/oauth2/PasswordGrantTokenServiceTest.kt b/oauth2-server-core/src/test/java/nl/myndocs/oauth2/PasswordGrantTokenServiceTest.kt index 9282ba5..4259549 100644 --- a/oauth2-server-core/src/test/java/nl/myndocs/oauth2/PasswordGrantTokenServiceTest.kt +++ b/oauth2-server-core/src/test/java/nl/myndocs/oauth2/PasswordGrantTokenServiceTest.kt @@ -1,7 +1,6 @@ package nl.myndocs.oauth2 import io.mockk.every -import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension @@ -13,22 +12,29 @@ import nl.myndocs.oauth2.exception.InvalidClientException import nl.myndocs.oauth2.exception.InvalidIdentityException import nl.myndocs.oauth2.exception.InvalidRequestException import nl.myndocs.oauth2.exception.InvalidScopeException +import nl.myndocs.oauth2.grant.GrantingCall +import nl.myndocs.oauth2.grant.authorize import nl.myndocs.oauth2.identity.Identity import nl.myndocs.oauth2.identity.IdentityService +import nl.myndocs.oauth2.request.CallContext import nl.myndocs.oauth2.request.PasswordGrantRequest import nl.myndocs.oauth2.token.AccessToken import nl.myndocs.oauth2.token.RefreshToken import nl.myndocs.oauth2.token.TokenStore import nl.myndocs.oauth2.token.converter.AccessTokenConverter import nl.myndocs.oauth2.token.converter.CodeTokenConverter +import nl.myndocs.oauth2.token.converter.Converters import nl.myndocs.oauth2.token.converter.RefreshTokenConverter import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.time.Instant @ExtendWith(MockKExtension::class) internal class PasswordGrantTokenServiceTest { + @MockK + lateinit var callContext: CallContext @MockK lateinit var identityService: IdentityService @MockK @@ -42,9 +48,22 @@ internal class PasswordGrantTokenServiceTest { @MockK lateinit var codeTokenConverter: CodeTokenConverter - @InjectMockKs - lateinit var tokenService: Oauth2TokenService - + lateinit var grantingCall: GrantingCall + + @BeforeEach + fun initialize() { + grantingCall = object : GrantingCall { + override val callContext = this@PasswordGrantTokenServiceTest.callContext + override val identityService = this@PasswordGrantTokenServiceTest.identityService + override val clientService = this@PasswordGrantTokenServiceTest.clientService + override val tokenStore = this@PasswordGrantTokenServiceTest.tokenStore + override val converters = Converters( + this@PasswordGrantTokenServiceTest.accessTokenConverter, + this@PasswordGrantTokenServiceTest.refreshTokenConverter, + this@PasswordGrantTokenServiceTest.codeTokenConverter + ) + } + } val clientId = "client-foo" val clientSecret = "client-bar" val username = "user-foo" @@ -76,7 +95,7 @@ internal class PasswordGrantTokenServiceTest { every { refreshTokenConverter.convertToToken(username, clientId, requestScopes) } returns refreshToken every { accessTokenConverter.convertToToken(username, clientId, requestScopes, refreshToken) } returns accessToken - tokenService.authorize(passwordGrantRequest) + grantingCall.authorize(passwordGrantRequest) verify { tokenStore.storeAccessToken(accessToken) } } @@ -87,7 +106,7 @@ internal class PasswordGrantTokenServiceTest { assertThrows( InvalidClientException::class.java - ) { tokenService.authorize(passwordGrantRequest) } + ) { grantingCall.authorize(passwordGrantRequest) } } @Test @@ -98,7 +117,7 @@ internal class PasswordGrantTokenServiceTest { assertThrows( InvalidClientException::class.java - ) { tokenService.authorize(passwordGrantRequest) } + ) { grantingCall.authorize(passwordGrantRequest) } } @Test @@ -117,7 +136,7 @@ internal class PasswordGrantTokenServiceTest { assertThrows( InvalidRequestException::class.java - ) { tokenService.authorize(passwordGrantRequest) } + ) { grantingCall.authorize(passwordGrantRequest) } } @Test @@ -136,7 +155,7 @@ internal class PasswordGrantTokenServiceTest { assertThrows( InvalidRequestException::class.java - ) { tokenService.authorize(passwordGrantRequest) } + ) { grantingCall.authorize(passwordGrantRequest) } } @Test @@ -151,7 +170,7 @@ internal class PasswordGrantTokenServiceTest { assertThrows( InvalidIdentityException::class.java - ) { tokenService.authorize(passwordGrantRequest) } + ) { grantingCall.authorize(passwordGrantRequest) } } @Test @@ -167,7 +186,7 @@ internal class PasswordGrantTokenServiceTest { assertThrows( InvalidScopeException::class.java - ) { tokenService.authorize(passwordGrantRequest) } + ) { grantingCall.authorize(passwordGrantRequest) } } @Test @@ -183,7 +202,7 @@ internal class PasswordGrantTokenServiceTest { assertThrows( InvalidScopeException::class.java - ) { tokenService.authorize(passwordGrantRequest) } + ) { grantingCall.authorize(passwordGrantRequest) } } @Test @@ -210,6 +229,6 @@ internal class PasswordGrantTokenServiceTest { every { refreshTokenConverter.convertToToken(username, clientId, requestScopes) } returns refreshToken every { accessTokenConverter.convertToToken(username, clientId, requestScopes, refreshToken) } returns accessToken - tokenService.authorize(passwordGrantRequest) + grantingCall.authorize(passwordGrantRequest) } } \ No newline at end of file diff --git a/oauth2-server-core/src/test/java/nl/myndocs/oauth2/RefreshTokenGrantTokenServiceTest.kt b/oauth2-server-core/src/test/java/nl/myndocs/oauth2/RefreshTokenGrantTokenServiceTest.kt index 96a428c..f8ec5d9 100644 --- a/oauth2-server-core/src/test/java/nl/myndocs/oauth2/RefreshTokenGrantTokenServiceTest.kt +++ b/oauth2-server-core/src/test/java/nl/myndocs/oauth2/RefreshTokenGrantTokenServiceTest.kt @@ -1,7 +1,6 @@ package nl.myndocs.oauth2 import io.mockk.every -import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension @@ -12,22 +11,29 @@ import nl.myndocs.oauth2.client.ClientService import nl.myndocs.oauth2.exception.InvalidClientException import nl.myndocs.oauth2.exception.InvalidGrantException import nl.myndocs.oauth2.exception.InvalidRequestException +import nl.myndocs.oauth2.grant.GrantingCall +import nl.myndocs.oauth2.grant.refresh import nl.myndocs.oauth2.identity.Identity import nl.myndocs.oauth2.identity.IdentityService +import nl.myndocs.oauth2.request.CallContext import nl.myndocs.oauth2.request.RefreshTokenRequest import nl.myndocs.oauth2.token.AccessToken import nl.myndocs.oauth2.token.RefreshToken import nl.myndocs.oauth2.token.TokenStore import nl.myndocs.oauth2.token.converter.AccessTokenConverter import nl.myndocs.oauth2.token.converter.CodeTokenConverter +import nl.myndocs.oauth2.token.converter.Converters import nl.myndocs.oauth2.token.converter.RefreshTokenConverter import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.time.Instant @ExtendWith(MockKExtension::class) internal class RefreshTokenGrantTokenServiceTest { + @MockK + lateinit var callContext: CallContext @MockK lateinit var identityService: IdentityService @MockK @@ -41,9 +47,22 @@ internal class RefreshTokenGrantTokenServiceTest { @MockK lateinit var codeTokenConverter: CodeTokenConverter - @InjectMockKs - lateinit var tokenService: Oauth2TokenService - + lateinit var grantingCall: GrantingCall + + @BeforeEach + fun initialize() { + grantingCall = object : GrantingCall { + override val callContext = this@RefreshTokenGrantTokenServiceTest.callContext + override val identityService = this@RefreshTokenGrantTokenServiceTest.identityService + override val clientService = this@RefreshTokenGrantTokenServiceTest.clientService + override val tokenStore = this@RefreshTokenGrantTokenServiceTest.tokenStore + override val converters = Converters( + this@RefreshTokenGrantTokenServiceTest.accessTokenConverter, + this@RefreshTokenGrantTokenServiceTest.refreshTokenConverter, + this@RefreshTokenGrantTokenServiceTest.codeTokenConverter + ) + } + } val clientId = "client-foo" val clientSecret = "client-bar" val refreshToken = "refresh-token" @@ -72,7 +91,7 @@ internal class RefreshTokenGrantTokenServiceTest { every { refreshTokenConverter.convertToToken(token) } returns newRefreshToken every { accessTokenConverter.convertToToken(username, clientId, scopes, newRefreshToken) } returns accessToken - tokenService.refresh(refreshTokenRequest) + grantingCall.refresh(refreshTokenRequest) verify { tokenStore.storeAccessToken(accessToken) } @@ -93,7 +112,7 @@ internal class RefreshTokenGrantTokenServiceTest { Assertions.assertThrows( InvalidRequestException::class.java - ) { tokenService.refresh(refreshTokenRequest) } + ) { grantingCall.refresh(refreshTokenRequest) } } @Test @@ -102,7 +121,7 @@ internal class RefreshTokenGrantTokenServiceTest { Assertions.assertThrows( InvalidClientException::class.java - ) { tokenService.refresh(refreshTokenRequest) } + ) { grantingCall.refresh(refreshTokenRequest) } } @Test @@ -113,7 +132,7 @@ internal class RefreshTokenGrantTokenServiceTest { Assertions.assertThrows( InvalidClientException::class.java - ) { tokenService.refresh(refreshTokenRequest) } + ) { grantingCall.refresh(refreshTokenRequest) } } @Test @@ -127,6 +146,6 @@ internal class RefreshTokenGrantTokenServiceTest { Assertions.assertThrows( InvalidGrantException::class.java - ) { tokenService.refresh(refreshTokenRequest) } + ) { grantingCall.refresh(refreshTokenRequest) } } } \ No newline at end of file From a4b597ea8c96541e57e3020b78f5013350f90b4e Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 14 Jan 2019 21:34:10 +0100 Subject: [PATCH 4/6] More documentation about configuration --- README.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/README.md b/README.md index e05c11c..5b33392 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,70 @@ authorizationEndpoint = "/custom/authorize" tokenInfoEndpoint = "/custom/tokeninfo" ``` +### In memory +In memory implementations are provided to easily setup the project. + +#### Identity +On the `InMemoryIdentity` identities can be registered. These are normally your users: +```kotlin +identityService = InMemoryIdentity() + .identity { + username = "foo-1" + password = "bar" + } + .identity { + username = "foo-2" + password = "bar" + } +``` + +#### Client +On the `InMemoryClient` clients can be registered: +```kotlin +clientService = InMemoryClient() + .client { + clientId = "app1-client" + clientSecret = "testpass" + scopes = setOf("admin") + redirectUris = setOf("https://localhost:8080/callback") + authorizedGrantTypes = setOf( + AuthorizedGrantType.AUTHORIZATION_CODE, + AuthorizedGrantType.PASSWORD, + AuthorizedGrantType.IMPLICIT, + AuthorizedGrantType.REFRESH_TOKEN + ) + } + .client { + clientId = "app2-client" + clientSecret = "testpass" + scopes = setOf("user") + redirectUris = setOf("https://localhost:8080/callback") + authorizedGrantTypes = setOf( + AuthorizedGrantType.AUTHORIZATION_CODE + ) + } +``` + +#### Token store +The `InMemoryTokenStore` stores all kinds of tokens. +```kotlin +tokenStore = InMemoryTokenStore() +``` + +### Converters + +#### Access token converter +By default `UUIDAccessTokenConverter` is used. With a default time-out of 1 hour. To override the time-out for example to half an hour: +```kotlin +accessTokenConverter = UUIDAccessTokenConverter(1800) +``` +#### Refresh token converter +By default `UUIDRefreshTokenConverter` is used. With a default time-out of 1 hour. To override the time-out for example to half an hour: +```kotlin +refreshTokenConverter = UUIDRefreshTokenConverter(1800) +``` +#### Code token converter +By default `UUIDCodeTokenConverter` is used. With a default time-out of 5 minutes. To override the time-out for example 2 minutes: +```kotlin +codeTokenConverter = UUIDCodeTokenConverter(120) +``` \ No newline at end of file From e7c8b26f98c41aea6e88428420d06d195056d6a4 Mon Sep 17 00:00:00 2001 From: "A. Bos" Date: Mon, 14 Jan 2019 21:39:47 +0100 Subject: [PATCH 5/6] Jwt converters (#48) * JWT converter * Use interface instead of callback to have more control --- oauth2-server-jwt/pom.xml | 27 ++++++++++++++++ .../nl/myndocs/convert/DefaultJwtBuilder.kt | 20 ++++++++++++ .../convert/JwtAccessTokenConverter.kt | 32 +++++++++++++++++++ .../java/nl/myndocs/convert/JwtBuilder.kt | 7 ++++ .../convert/JwtRefreshTokenConverter.kt | 29 +++++++++++++++++ pom.xml | 1 + 6 files changed, 116 insertions(+) create mode 100644 oauth2-server-jwt/pom.xml create mode 100644 oauth2-server-jwt/src/main/java/nl/myndocs/convert/DefaultJwtBuilder.kt create mode 100644 oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtAccessTokenConverter.kt create mode 100644 oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtBuilder.kt create mode 100644 oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtRefreshTokenConverter.kt diff --git a/oauth2-server-jwt/pom.xml b/oauth2-server-jwt/pom.xml new file mode 100644 index 0000000..9ccd3bd --- /dev/null +++ b/oauth2-server-jwt/pom.xml @@ -0,0 +1,27 @@ + + + + kotlin-oauth2-server + nl.myndocs + 0.3.2-SNAPSHOT + + 4.0.0 + + oauth2-server-jwt + + + + nl.myndocs + oauth2-server-core + ${project.version} + provided + + + com.auth0 + java-jwt + 3.5.0 + + + \ No newline at end of file diff --git a/oauth2-server-jwt/src/main/java/nl/myndocs/convert/DefaultJwtBuilder.kt b/oauth2-server-jwt/src/main/java/nl/myndocs/convert/DefaultJwtBuilder.kt new file mode 100644 index 0000000..2b44c1e --- /dev/null +++ b/oauth2-server-jwt/src/main/java/nl/myndocs/convert/DefaultJwtBuilder.kt @@ -0,0 +1,20 @@ +package nl.myndocs.convert + +import com.auth0.jwt.JWT +import java.time.Instant +import java.util.* + +object DefaultJwtBuilder : JwtBuilder { + override fun buildJwt(username: String?, clientId: String, requestedScopes: Set, expiresInSeconds: Long) = + JWT.create() + .withIssuedAt(Date.from(Instant.now())) + .withExpiresAt( + Date.from( + Instant.now() + .plusSeconds(expiresInSeconds) + ) + ) + .withClaim("client_id", clientId) + .withArrayClaim("scopes", requestedScopes.toTypedArray()) + .let { withBuilder -> if (username != null) withBuilder.withClaim("username", username) else withBuilder } +} \ No newline at end of file diff --git a/oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtAccessTokenConverter.kt b/oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtAccessTokenConverter.kt new file mode 100644 index 0000000..a794724 --- /dev/null +++ b/oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtAccessTokenConverter.kt @@ -0,0 +1,32 @@ +package nl.myndocs.convert + +import com.auth0.jwt.algorithms.Algorithm +import nl.myndocs.oauth2.token.AccessToken +import nl.myndocs.oauth2.token.RefreshToken +import nl.myndocs.oauth2.token.converter.AccessTokenConverter +import java.time.Instant + +class JwtAccessTokenConverter( + private val algorithm: Algorithm, + private val accessTokenExpireInSeconds: Int = 3600, + private val jwtBuilder: JwtBuilder = DefaultJwtBuilder +) : AccessTokenConverter { + override fun convertToToken(username: String?, clientId: String, requestedScopes: Set, refreshToken: RefreshToken?): AccessToken { + val jwtBuilder = jwtBuilder.buildJwt( + username, + clientId, + requestedScopes, + accessTokenExpireInSeconds.toLong() + ) + + return AccessToken( + jwtBuilder.sign(algorithm), + "bearer", + Instant.now().plusSeconds(accessTokenExpireInSeconds.toLong()), + username, + clientId, + requestedScopes, + refreshToken + ) + } +} \ No newline at end of file diff --git a/oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtBuilder.kt b/oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtBuilder.kt new file mode 100644 index 0000000..800b09b --- /dev/null +++ b/oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtBuilder.kt @@ -0,0 +1,7 @@ +package nl.myndocs.convert + +import com.auth0.jwt.JWTCreator + +interface JwtBuilder { + fun buildJwt(username: String?, clientId: String, requestedScopes: Set, expiresInSeconds: Long): JWTCreator.Builder +} \ No newline at end of file diff --git a/oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtRefreshTokenConverter.kt b/oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtRefreshTokenConverter.kt new file mode 100644 index 0000000..e633fb8 --- /dev/null +++ b/oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtRefreshTokenConverter.kt @@ -0,0 +1,29 @@ +package nl.myndocs.convert + +import com.auth0.jwt.algorithms.Algorithm +import nl.myndocs.oauth2.token.RefreshToken +import nl.myndocs.oauth2.token.converter.RefreshTokenConverter +import java.time.Instant + +class JwtRefreshTokenConverter( + private val algorithm: Algorithm, + private val refreshTokenExpireInSeconds: Int = 86400, + private val jwtBuilder: JwtBuilder = DefaultJwtBuilder +) : RefreshTokenConverter { + override fun convertToToken(username: String?, clientId: String, requestedScopes: Set): RefreshToken { + val jwtBuilder = jwtBuilder.buildJwt( + username, + clientId, + requestedScopes, + refreshTokenExpireInSeconds.toLong() + ) + + return RefreshToken( + jwtBuilder.sign(algorithm), + Instant.now().plusSeconds(refreshTokenExpireInSeconds.toLong()), + username, + clientId, + requestedScopes + ) + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index d3bd6c0..61c237b 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ oauth2-server-javalin oauth2-server-sparkjava oauth2-server-http4k + oauth2-server-jwt From 5ec6118d8e8f34ba20680a6052591c805755f340 Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 14 Jan 2019 21:42:01 +0100 Subject: [PATCH 6/6] 0.4.0 release --- oauth2-server-client-inmemory/pom.xml | 2 +- oauth2-server-core/pom.xml | 2 +- oauth2-server-http4k/pom.xml | 2 +- oauth2-server-identity-inmemory/pom.xml | 2 +- oauth2-server-javalin/pom.xml | 2 +- oauth2-server-json/pom.xml | 2 +- oauth2-server-jwt/pom.xml | 2 +- oauth2-server-ktor/pom.xml | 2 +- oauth2-server-sparkjava/pom.xml | 2 +- oauth2-server-token-store-inmemory/pom.xml | 2 +- pom.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/oauth2-server-client-inmemory/pom.xml b/oauth2-server-client-inmemory/pom.xml index e8b1011..829a54c 100644 --- a/oauth2-server-client-inmemory/pom.xml +++ b/oauth2-server-client-inmemory/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.2-SNAPSHOT + 0.4.0 4.0.0 diff --git a/oauth2-server-core/pom.xml b/oauth2-server-core/pom.xml index f4dbd2f..fd69de6 100644 --- a/oauth2-server-core/pom.xml +++ b/oauth2-server-core/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.2-SNAPSHOT + 0.4.0 4.0.0 diff --git a/oauth2-server-http4k/pom.xml b/oauth2-server-http4k/pom.xml index 7f8466a..632dbf4 100644 --- a/oauth2-server-http4k/pom.xml +++ b/oauth2-server-http4k/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.2-SNAPSHOT + 0.4.0 4.0.0 diff --git a/oauth2-server-identity-inmemory/pom.xml b/oauth2-server-identity-inmemory/pom.xml index cc814a1..c77bcce 100644 --- a/oauth2-server-identity-inmemory/pom.xml +++ b/oauth2-server-identity-inmemory/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.2-SNAPSHOT + 0.4.0 4.0.0 diff --git a/oauth2-server-javalin/pom.xml b/oauth2-server-javalin/pom.xml index 6e8eb00..0461baa 100644 --- a/oauth2-server-javalin/pom.xml +++ b/oauth2-server-javalin/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.2-SNAPSHOT + 0.4.0 4.0.0 diff --git a/oauth2-server-json/pom.xml b/oauth2-server-json/pom.xml index be15c53..3364729 100644 --- a/oauth2-server-json/pom.xml +++ b/oauth2-server-json/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.2-SNAPSHOT + 0.4.0 4.0.0 diff --git a/oauth2-server-jwt/pom.xml b/oauth2-server-jwt/pom.xml index 9ccd3bd..e873dec 100644 --- a/oauth2-server-jwt/pom.xml +++ b/oauth2-server-jwt/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.2-SNAPSHOT + 0.4.0 4.0.0 diff --git a/oauth2-server-ktor/pom.xml b/oauth2-server-ktor/pom.xml index 446867b..81ee9e2 100644 --- a/oauth2-server-ktor/pom.xml +++ b/oauth2-server-ktor/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.2-SNAPSHOT + 0.4.0 4.0.0 diff --git a/oauth2-server-sparkjava/pom.xml b/oauth2-server-sparkjava/pom.xml index d8208a4..67287b6 100644 --- a/oauth2-server-sparkjava/pom.xml +++ b/oauth2-server-sparkjava/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.2-SNAPSHOT + 0.4.0 4.0.0 diff --git a/oauth2-server-token-store-inmemory/pom.xml b/oauth2-server-token-store-inmemory/pom.xml index 15c869d..6b217a7 100644 --- a/oauth2-server-token-store-inmemory/pom.xml +++ b/oauth2-server-token-store-inmemory/pom.xml @@ -5,7 +5,7 @@ kotlin-oauth2-server nl.myndocs - 0.3.2-SNAPSHOT + 0.4.0 4.0.0 diff --git a/pom.xml b/pom.xml index 61c237b..551cad7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ nl.myndocs kotlin-oauth2-server pom - 0.3.2-SNAPSHOT + 0.4.0 1.3.0