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