diff --git a/README.md b/README.md
index c8c33d6..5b33392 100644
--- a/README.md
+++ b/README.md
@@ -51,210 +51,94 @@ 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:
+### 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
-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)
+tokenEndpoint = "/custom/token"
+authorizationEndpoint = "/custom/authorize"
+tokenInfoEndpoint = "/custom/tokeninfo"
```
-## Javalin
-The following dependency is required along with the dependencies described in Setup
-```xml
-
- nl.myndocs
- oauth2-server-javalin
- ${myndocs.oauth.version}
-
-```
+### In memory
+In memory implementations are provided to easily setup the project.
-In memory example for Javalin:
+#### Identity
+On the `InMemoryIdentity` identities can be registered. These are normally your users:
```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()
- }
-
+identityService = InMemoryIdentity()
+ .identity {
+ username = "foo-1"
+ password = "bar"
}
-}.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()
+ .identity {
+ username = "foo-2"
+ password = "bar"
}
-}
-```
-## 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:
+#### Client
+On the `InMemoryClient` clients can be registered:
```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()
- }
+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
+ )
}
-
- app.asServer(Jetty(9000)).start()
+ .client {
+ clientId = "app2-client"
+ clientSecret = "testpass"
+ scopes = setOf("user")
+ redirectUris = setOf("https://localhost:8080/callback")
+ authorizedGrantTypes = setOf(
+ AuthorizedGrantType.AUTHORIZATION_CODE
+ )
+ }
```
-**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.
-
+#### Token store
+The `InMemoryTokenStore` stores all kinds of tokens.
```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
+tokenStore = InMemoryTokenStore()
```
-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.
+### 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
-fun clientOf(clientId: String): Client?
-
-fun validClient(client: Client, clientSecret: String): Boolean
+accessTokenConverter = UUIDAccessTokenConverter(1800)
```
-
-## Token store
-The following methods have to be implemented for a token store.
-
+#### 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
-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)
-
+refreshTokenConverter = UUIDRefreshTokenConverter(1800)
```
-
-When `AccessToken` is passed to `storeAccessToken` and it contains a `RefreshToken`, then `storeAccessToken` is also responsible for saving the refresh token.
+#### 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
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-client-inmemory/pom.xml b/oauth2-server-client-inmemory/pom.xml
index 091d177..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.1
+ 0.4.0
4.0.0
diff --git a/oauth2-server-core/pom.xml b/oauth2-server-core/pom.xml
index 6861d77..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.1
+ 0.4.0
4.0.0
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..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
@@ -1,21 +1,24 @@
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.grant.redirect
+import nl.myndocs.oauth2.grant.tokenInfo
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>,
+ private val grantingCallFactory: (CallContext) -> GrantingCall
) {
companion object {
const val METHOD_POST = "post"
@@ -42,85 +45,39 @@ 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 = grantingCallFactory(callContext)
+
+ 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,
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"],
@@ -148,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"],
@@ -199,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)
@@ -222,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 33732cb..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.CallRouter
-import nl.myndocs.oauth2.TokenService
+import nl.myndocs.oauth2.grant.*
import nl.myndocs.oauth2.identity.TokenInfo
+import nl.myndocs.oauth2.request.CallContext
internal object CallRouterBuilder {
class Configuration {
@@ -15,21 +16,20 @@ 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,
- configuration.tokenInfoCallback
+ configuration.tokenInfoCallback,
+ listOf Granter>(
+ { grantPassword() },
+ { grantAuthorizationCode() },
+ { grantClientCredentials() },
+ { grantRefreshToken() }
+ ) + 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 e73177f..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,21 +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) {
@@ -40,16 +39,41 @@ object ConfigurationBuilder {
callRouterConfiguration.tokenInfoCallback = value
}
+ var granters: List Granter>
+ get() = callRouterConfiguration.granters
+ set(value) {
+ callRouterConfiguration.granters = value
+ }
+
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
new file mode 100644
index 0000000..f2bbe5a
--- /dev/null
+++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterDefault.kt
@@ -0,0 +1,121 @@
+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 = 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 = 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 = 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 = authorize(
+ AuthorizationCodeRequest(
+ callContext.formParameters["client_id"],
+ callContext.formParameters["client_secret"],
+ callContext.formParameters["code"],
+ callContext.formParameters["redirect_uri"]
+ )
+ )
+
+ 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/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..b219253
--- /dev/null
+++ b/oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantingCall.kt
@@ -0,0 +1,15 @@
+package nl.myndocs.oauth2.grant
+
+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 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
diff --git a/oauth2-server-http4k/pom.xml b/oauth2-server-http4k/pom.xml
index c32cb6e..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.1
+ 0.4.0
4.0.0
diff --git a/oauth2-server-identity-inmemory/pom.xml b/oauth2-server-identity-inmemory/pom.xml
index fb31465..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.1
+ 0.4.0
4.0.0
diff --git a/oauth2-server-javalin/pom.xml b/oauth2-server-javalin/pom.xml
index 23e2f92..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.1
+ 0.4.0
4.0.0
diff --git a/oauth2-server-json/pom.xml b/oauth2-server-json/pom.xml
index 8630d82..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.1
+ 0.4.0
4.0.0
diff --git a/oauth2-server-jwt/pom.xml b/oauth2-server-jwt/pom.xml
new file mode 100644
index 0000000..e873dec
--- /dev/null
+++ b/oauth2-server-jwt/pom.xml
@@ -0,0 +1,27 @@
+
+
+
+ kotlin-oauth2-server
+ nl.myndocs
+ 0.4.0
+
+ 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/oauth2-server-ktor/pom.xml b/oauth2-server-ktor/pom.xml
index d5c82c3..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.1
+ 0.4.0
4.0.0
diff --git a/oauth2-server-sparkjava/pom.xml b/oauth2-server-sparkjava/pom.xml
index bc79622..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.1
+ 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 ae0d9f3..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.1
+ 0.4.0
4.0.0
diff --git a/pom.xml b/pom.xml
index e68ef61..551cad7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
nl.myndocs
kotlin-oauth2-server
pom
- 0.3.1
+ 0.4.0
1.3.0
@@ -25,6 +25,7 @@
oauth2-server-javalin
oauth2-server-sparkjava
oauth2-server-http4k
+ oauth2-server-jwt