├── oauth2-server-core ├── src │ ├── main │ │ └── java │ │ │ └── nl │ │ │ └── myndocs │ │ │ └── oauth2 │ │ │ ├── exception │ │ │ ├── InvalidIdentityException.kt │ │ │ ├── InvalidClientException.kt │ │ │ ├── InvalidRequestException.kt │ │ │ ├── InvalidGrantException.kt │ │ │ ├── InvalidScopeException.kt │ │ │ ├── OauthExceptionUtil.kt │ │ │ ├── NoRoutesFoundException.kt │ │ │ └── OauthException.kt │ │ │ ├── grant │ │ │ ├── Granter.kt │ │ │ ├── GrantBuilder.kt │ │ │ ├── GrantingCall.kt │ │ │ ├── CallRouterRefresh.kt │ │ │ ├── CallRouterRedirect.kt │ │ │ ├── CallRouterDefault.kt │ │ │ └── CallRouterAuthorize.kt │ │ │ ├── authenticator │ │ │ ├── Credentials.kt │ │ │ ├── Authenticator.kt │ │ │ └── IdentityScopeVerifier.kt │ │ │ ├── router │ │ │ ├── RedirectRouterResponse.kt │ │ │ └── RedirectRouter.kt │ │ │ ├── config │ │ │ ├── Configuration.kt │ │ │ ├── CallRouterBuilder.kt │ │ │ └── ConfigurationBuilder.kt │ │ │ ├── request │ │ │ ├── ClientRequest.kt │ │ │ ├── CallContextHeader.kt │ │ │ ├── RedirectTokenRequest.kt │ │ │ ├── RedirectAuthorizationCodeRequest.kt │ │ │ ├── RefreshTokenRequest.kt │ │ │ ├── ClientCredentialsRequest.kt │ │ │ ├── AuthorizationCodeRequest.kt │ │ │ ├── PasswordGrantRequest.kt │ │ │ ├── CallContext.kt │ │ │ └── auth │ │ │ │ ├── CallContextBasicAuthenticator.kt │ │ │ │ ├── BasicAuthenticator.kt │ │ │ │ └── BasicAuth.kt │ │ │ ├── identity │ │ │ ├── Identity.kt │ │ │ ├── TokenInfo.kt │ │ │ └── IdentityService.kt │ │ │ ├── client │ │ │ ├── ClientService.kt │ │ │ ├── Client.kt │ │ │ └── AuthorizedGrantType.kt │ │ │ ├── response │ │ │ ├── AccessTokenResponder.kt │ │ │ └── DefaultAccessTokenResponder.kt │ │ │ ├── token │ │ │ ├── converter │ │ │ │ ├── Converters.kt │ │ │ │ ├── CodeTokenConverter.kt │ │ │ │ ├── AccessTokenConverter.kt │ │ │ │ ├── RefreshTokenConverter.kt │ │ │ │ ├── UUIDRefreshTokenConverter.kt │ │ │ │ ├── UUIDCodeTokenConverter.kt │ │ │ │ └── UUIDAccessTokenConverter.kt │ │ │ ├── RefreshToken.kt │ │ │ ├── ExpirableToken.kt │ │ │ ├── CodeToken.kt │ │ │ ├── AccessToken.kt │ │ │ └── TokenStore.kt │ │ │ ├── scope │ │ │ └── ScopeParser.kt │ │ │ └── CallRouter.kt │ └── test │ │ └── java │ │ └── nl │ │ └── myndocs │ │ └── oauth2 │ │ ├── scope │ │ └── ScopeParserTest.kt │ │ ├── request │ │ └── auth │ │ │ └── BasicAuthenticatorTest.kt │ │ ├── ClientCredentialsTokenServiceTest.kt │ │ ├── RefreshTokenGrantTokenServiceTest.kt │ │ ├── AuthorizationCodeGrantTokenServiceTest.kt │ │ └── PasswordGrantTokenServiceTest.kt └── pom.xml ├── oauth2-server-json ├── src │ └── main │ │ └── java │ │ └── nl │ │ └── myndocs │ │ └── oauth2 │ │ └── json │ │ └── JsonMapper.kt └── pom.xml ├── oauth2-server-identity-inmemory ├── src │ └── main │ │ └── java │ │ └── nl │ │ └── myndocs │ │ └── oauth2 │ │ └── identity │ │ └── inmemory │ │ ├── IdentityConfiguration.kt │ │ └── InMemoryIdentity.kt └── pom.xml ├── oauth2-server-jwt ├── src │ └── main │ │ └── java │ │ └── nl │ │ └── myndocs │ │ └── convert │ │ ├── JwtBuilder.kt │ │ ├── DefaultJwtBuilder.kt │ │ ├── JwtRefreshTokenConverter.kt │ │ └── JwtAccessTokenConverter.kt └── pom.xml ├── .gitignore ├── oauth2-server-client-inmemory ├── src │ └── main │ │ └── java │ │ └── nl │ │ └── myndocs │ │ └── oauth2 │ │ └── client │ │ └── inmemory │ │ ├── ClientConfiguration.kt │ │ └── InMemoryClient.kt └── pom.xml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── maven.yml ├── Jenkinsfile ├── oauth2-server-ktor ├── src │ ├── main │ │ └── java │ │ │ └── nl │ │ │ └── myndocs │ │ │ └── oauth2 │ │ │ └── ktor │ │ │ └── feature │ │ │ ├── config │ │ │ └── KtorConfiguration.kt │ │ │ ├── Oauth2ServerFeature.kt │ │ │ └── request │ │ │ └── KtorCallContext.kt │ └── test │ │ └── java │ │ └── nl │ │ └── myndocs │ │ └── oauth2 │ │ └── ktor │ │ └── integration │ │ └── KtorIntegrationTest.kt └── pom.xml ├── oauth2-server-http4k ├── src │ ├── main │ │ └── java │ │ │ └── nl │ │ │ └── myndocs │ │ │ └── oauth2 │ │ │ └── http4k │ │ │ ├── response │ │ │ └── ResponseBuilder.kt │ │ │ ├── request │ │ │ └── Http4kCallContext.kt │ │ │ └── Oauth2Server.kt │ └── test │ │ └── java │ │ └── nl │ │ └── myndocs │ │ └── oauth2 │ │ └── http4k │ │ └── integration │ │ └── Http4kIntegrationTest.kt └── pom.xml ├── oauth2-server-sparkjava ├── src │ ├── test │ │ └── java │ │ │ └── nl │ │ │ └── myndocs │ │ │ └── oauth2 │ │ │ └── sparkjava │ │ │ └── integration │ │ │ └── SparkjavaIntegrationTest.kt │ └── main │ │ └── java │ │ └── nl │ │ └── myndocs │ │ └── oauth2 │ │ └── sparkjava │ │ ├── request │ │ └── SparkjavaCallContext.kt │ │ └── Oauth2Server.kt └── pom.xml ├── oauth2-server-javalin ├── src │ ├── test │ │ └── java │ │ │ └── nl │ │ │ └── myndocs │ │ │ └── oauth2 │ │ │ └── javalin │ │ │ └── integration │ │ │ └── JavalinIntegrationTest.kt │ └── main │ │ └── java │ │ └── nl │ │ └── myndocs │ │ └── oauth2 │ │ └── javalin │ │ ├── request │ │ └── JavalinCallContext.kt │ │ └── Oauth2Server.kt └── pom.xml ├── oauth2-server-token-store-inmemory ├── pom.xml └── src │ └── main │ └── java │ └── nl │ └── myndocs │ └── oauth2 │ └── tokenstore │ └── inmemory │ └── InMemoryTokenStore.kt ├── oauth2-server-hexagon ├── src │ ├── test │ │ └── java │ │ │ └── nl │ │ │ └── myndocs │ │ │ └── oauth2 │ │ │ └── hexagon │ │ │ └── integration │ │ │ └── HexagonIntegrationTest.kt │ └── main │ │ └── java │ │ └── nl │ │ └── myndocs │ │ └── oauth2 │ │ └── hexagon │ │ ├── Oauth2Server.kt │ │ └── request │ │ └── HexagonCallContext.kt └── pom.xml ├── docs ├── sparkjava.md ├── javalin.md ├── ktor.md └── http4k.md ├── oauth2-server-integration-base ├── pom.xml └── src │ └── main │ └── java │ └── nl │ └── myndocs │ └── oauth2 │ └── integration │ └── BaseIntegrationTest.kt ├── README.md ├── pom.xml └── LICENSE /oauth2-server-core/src/main/java/nl/myndocs/oauth2/exception/InvalidIdentityException.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.exception 2 | 3 | class InvalidIdentityException : InvalidGrantException() -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/Granter.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.grant 2 | 3 | class Granter( 4 | val grantType: String, 5 | val callback: () -> Unit 6 | ) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/authenticator/Credentials.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.authenticator 2 | 3 | data class Credentials(val username: String?, val password: String?) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantBuilder.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.grant 2 | 3 | fun granter(grantType: String, callback: () -> Unit) = Granter(grantType, callback) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/exception/InvalidClientException.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.exception 2 | 3 | class InvalidClientException : OauthException(OauthError.INVALID_CLIENT) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/router/RedirectRouterResponse.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.router 2 | 3 | data class RedirectRouterResponse( 4 | val successfulLogin: Boolean 5 | ) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/Configuration.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.config 2 | 3 | import nl.myndocs.oauth2.CallRouter 4 | 5 | data class Configuration(val callRouter: CallRouter) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/ClientRequest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request 2 | 3 | interface ClientRequest { 4 | val clientId: String? 5 | val clientSecret: String? 6 | } 7 | -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/identity/Identity.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.identity 2 | 3 | data class Identity( 4 | val username: String, 5 | val metadata: Map = mapOf() 6 | ) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/exception/InvalidRequestException.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.exception 2 | 3 | class InvalidRequestException(message: String) : OauthException(OauthError.INVALID_REQUEST, message) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/exception/InvalidGrantException.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.exception 2 | 3 | open class InvalidGrantException(message: String? = null) : OauthException(OauthError.INVALID_GRANT, message) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/client/ClientService.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.client 2 | 3 | interface ClientService { 4 | fun clientOf(clientId: String): Client? 5 | fun validClient(client: Client, clientSecret: String): Boolean 6 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/CallContextHeader.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request 2 | 3 | fun CallContext.headerCaseInsensitive(key: String) = headers 4 | .filter { it.key.equals(key, true) } 5 | .values 6 | .firstOrNull() -------------------------------------------------------------------------------- /oauth2-server-json/src/main/java/nl/myndocs/oauth2/json/JsonMapper.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.json 2 | 3 | import com.google.gson.Gson 4 | 5 | object JsonMapper { 6 | private val gson = Gson() 7 | 8 | fun toJson(content: Any) = gson.toJson(content) 9 | } -------------------------------------------------------------------------------- /oauth2-server-identity-inmemory/src/main/java/nl/myndocs/oauth2/identity/inmemory/IdentityConfiguration.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.identity.inmemory 2 | 3 | data class IdentityConfiguration( 4 | var username: String? = null, 5 | var password: String? = null 6 | ) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/client/Client.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.client 2 | 3 | data class Client( 4 | val clientId: String, 5 | val clientScopes: Set, 6 | val redirectUris: Set, 7 | val authorizedGrantTypes: Set 8 | ) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/exception/InvalidScopeException.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.exception 2 | 3 | class InvalidScopeException(val notAllowedScopes: Set) : 4 | OauthException(OauthError.INVALID_SCOPE, "Scopes not allowed: $notAllowedScopes") -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/identity/TokenInfo.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.identity 2 | 3 | import nl.myndocs.oauth2.client.Client 4 | 5 | data class TokenInfo( 6 | val identity: Identity?, 7 | val client: Client, 8 | val scopes: Set 9 | ) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/response/AccessTokenResponder.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.response 2 | 3 | import nl.myndocs.oauth2.token.AccessToken 4 | 5 | interface AccessTokenResponder { 6 | fun createResponse(accessToken: AccessToken): Map 7 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/RedirectTokenRequest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request 2 | 3 | class RedirectTokenRequest( 4 | val clientId: String?, 5 | val redirectUri: String?, 6 | val username: String?, 7 | val password: String?, 8 | val scope: String? 9 | ) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/converter/Converters.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token.converter 2 | 3 | data class Converters( 4 | val accessTokenConverter: AccessTokenConverter, 5 | val refreshTokenConverter: RefreshTokenConverter, 6 | val codeTokenConverter: CodeTokenConverter 7 | ) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/RedirectAuthorizationCodeRequest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request 2 | 3 | class RedirectAuthorizationCodeRequest( 4 | val clientId: String?, 5 | val redirectUri: String?, 6 | val username: String?, 7 | val password: String?, 8 | val scope: String? 9 | ) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/RefreshTokenRequest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request 2 | 3 | data class RefreshTokenRequest( 4 | override val clientId: String?, 5 | override val clientSecret: String?, 6 | val refreshToken: String? 7 | ) : ClientRequest { 8 | val grant_type = "refresh_token" 9 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/authenticator/Authenticator.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.authenticator 2 | 3 | import nl.myndocs.oauth2.client.Client 4 | import nl.myndocs.oauth2.identity.Identity 5 | 6 | interface Authenticator { 7 | fun validCredentials(forClient: Client, identity: Identity, password: String): Boolean 8 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/ClientCredentialsRequest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request 2 | 3 | data class ClientCredentialsRequest( 4 | override val clientId: String?, 5 | override val clientSecret: String?, 6 | val scope: String? 7 | ) : ClientRequest { 8 | val grant_type = "client_credentials" 9 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/router/RedirectRouter.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.router 2 | 3 | import nl.myndocs.oauth2.authenticator.Credentials 4 | import nl.myndocs.oauth2.request.CallContext 5 | 6 | interface RedirectRouter { 7 | fun route(callContext: CallContext, credentials: Credentials?): RedirectRouterResponse 8 | } -------------------------------------------------------------------------------- /oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtBuilder.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.convert 2 | 3 | import com.auth0.jwt.JWTCreator 4 | import nl.myndocs.oauth2.identity.Identity 5 | 6 | interface JwtBuilder { 7 | fun buildJwt(identity: Identity?, clientId: String, requestedScopes: Set, expiresInSeconds: Long): JWTCreator.Builder 8 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/exception/OauthExceptionUtil.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.exception 2 | 3 | fun OauthException.toMap(): Map = with(mutableMapOf("error" to error.errorName)) { 4 | if (errorDescription != null) { 5 | this["error_description"] = errorDescription 6 | } 7 | toMap() 8 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/AuthorizationCodeRequest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request 2 | 3 | data class AuthorizationCodeRequest( 4 | override val clientId: String?, 5 | override val clientSecret: String?, 6 | val code: String?, 7 | val redirectUri: String? 8 | ) : ClientRequest { 9 | val grant_type = "authorization_code" 10 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/exception/NoRoutesFoundException.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.exception 2 | 3 | class NoRoutesFoundException : Exception { 4 | constructor() : super() 5 | constructor(message: String?) : super(message) 6 | constructor(message: String?, cause: Throwable?) : super(message, cause) 7 | constructor(cause: Throwable?) : super(cause) 8 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/PasswordGrantRequest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request 2 | 3 | data class PasswordGrantRequest( 4 | override val clientId: String?, 5 | override val clientSecret: String?, 6 | val username: String?, 7 | val password: String?, 8 | val scope: String? 9 | ) : ClientRequest { 10 | val grant_type = "password" 11 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/client/AuthorizedGrantType.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.client 2 | 3 | object AuthorizedGrantType { 4 | const val IMPLICIT = "implicit" 5 | const val REFRESH_TOKEN = "refresh_token" 6 | const val PASSWORD = "password" 7 | const val AUTHORIZATION_CODE = "authorization_code" 8 | const val CLIENT_CREDENTIALS = "client_credentials" 9 | } 10 | -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/scope/ScopeParser.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.scope 2 | 3 | object ScopeParser { 4 | private const val SCOPE_SEPARATOR = " " 5 | 6 | fun parseScopes(scopes: String?): Set = 7 | if (!scopes.isNullOrBlank()) 8 | scopes 9 | .split(SCOPE_SEPARATOR) 10 | .toSet() 11 | else setOf() 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .idea 26 | **.iml -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/RefreshToken.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token 2 | 3 | import nl.myndocs.oauth2.identity.Identity 4 | import java.time.Instant 5 | 6 | data class RefreshToken( 7 | val refreshToken: String, 8 | override val expireTime: Instant, 9 | val identity: Identity?, 10 | val clientId: String, 11 | val scopes: Set 12 | ) : ExpirableToken -------------------------------------------------------------------------------- /oauth2-server-client-inmemory/src/main/java/nl/myndocs/oauth2/client/inmemory/ClientConfiguration.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.client.inmemory 2 | 3 | data class ClientConfiguration( 4 | var clientId: String? = null, 5 | var clientSecret: String? = null, 6 | var scopes: Set = setOf(), 7 | var redirectUris: Set = setOf(), 8 | var authorizedGrantTypes: Set = setOf() 9 | ) -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/ExpirableToken.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token 2 | 3 | import java.time.Instant 4 | import java.time.temporal.ChronoUnit 5 | 6 | interface ExpirableToken { 7 | val expireTime: Instant 8 | 9 | fun expiresIn(): Int = 10 | Instant.now().until(expireTime, ChronoUnit.SECONDS).toInt() 11 | 12 | fun expired(): Boolean = 13 | expiresIn() <= 0 14 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/CodeToken.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token 2 | 3 | import nl.myndocs.oauth2.identity.Identity 4 | import java.time.Instant 5 | 6 | data class CodeToken( 7 | val codeToken: String, 8 | override val expireTime: Instant, 9 | val identity: Identity, 10 | val clientId: String, 11 | val redirectUri: String, 12 | val scopes: Set 13 | ) : ExpirableToken -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/converter/CodeTokenConverter.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token.converter 2 | 3 | import nl.myndocs.oauth2.identity.Identity 4 | import nl.myndocs.oauth2.token.CodeToken 5 | 6 | interface CodeTokenConverter { 7 | fun convertToToken( 8 | identity: Identity, 9 | clientId: String, 10 | redirectUri: String, 11 | requestedScopes: Set 12 | ): CodeToken 13 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/authenticator/IdentityScopeVerifier.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.authenticator 2 | 3 | import nl.myndocs.oauth2.client.Client 4 | import nl.myndocs.oauth2.identity.Identity 5 | 6 | interface IdentityScopeVerifier { 7 | /** 8 | * Validate which scopes are allowed. Leave out the scopes which are not allowed 9 | */ 10 | fun allowedScopes(forClient: Client, identity: Identity, scopes: Set): Set 11 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/AccessToken.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token 2 | 3 | import nl.myndocs.oauth2.identity.Identity 4 | import java.time.Instant 5 | 6 | data class AccessToken( 7 | val accessToken: String, 8 | val tokenType: String, 9 | override val expireTime: Instant, 10 | val identity: Identity?, 11 | val clientId: String, 12 | val scopes: Set, 13 | val refreshToken: RefreshToken? 14 | ) : ExpirableToken -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. ... 13 | 2. ... 14 | 3. ... 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | 4 | tools { 5 | maven 'mvn-3.6.0' 6 | jdk 'jdk-8' 7 | } 8 | 9 | stages { 10 | stage('Cleanup') { 11 | steps { 12 | sh 'mvn clean' 13 | } 14 | } 15 | stage('Test') { 16 | steps { 17 | sh 'mvn test' 18 | } 19 | } 20 | } 21 | post { 22 | always { 23 | cleanWs() 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/CallContext.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request 2 | 3 | interface CallContext { 4 | val path: String 5 | val method: String 6 | val headers: Map 7 | val queryParameters: Map 8 | val formParameters: Map 9 | 10 | fun respondStatus(statusCode: Int) 11 | fun respondHeader(name: String, value: String) 12 | fun respondJson(content: Any) 13 | fun redirect(uri: String) 14 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/converter/AccessTokenConverter.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token.converter 2 | 3 | import nl.myndocs.oauth2.identity.Identity 4 | import nl.myndocs.oauth2.token.AccessToken 5 | import nl.myndocs.oauth2.token.RefreshToken 6 | 7 | interface AccessTokenConverter { 8 | fun convertToToken( 9 | identity: Identity?, 10 | clientId: String, 11 | requestedScopes: Set, 12 | refreshToken: RefreshToken? 13 | ): AccessToken 14 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/identity/IdentityService.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.identity 2 | 3 | import nl.myndocs.oauth2.authenticator.Authenticator 4 | import nl.myndocs.oauth2.authenticator.IdentityScopeVerifier 5 | import nl.myndocs.oauth2.client.Client 6 | 7 | interface IdentityService : Authenticator, IdentityScopeVerifier { 8 | /** 9 | * Find identity within a client and username 10 | * If not found return null 11 | */ 12 | fun identityOf(forClient: Client, username: String): Identity? 13 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/converter/RefreshTokenConverter.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token.converter 2 | 3 | import nl.myndocs.oauth2.identity.Identity 4 | import nl.myndocs.oauth2.token.RefreshToken 5 | 6 | interface RefreshTokenConverter { 7 | fun convertToToken(refreshToken: RefreshToken): RefreshToken = 8 | convertToToken(refreshToken.identity, refreshToken.clientId, refreshToken.scopes) 9 | 10 | fun convertToToken(identity: Identity?, clientId: String, requestedScopes: Set): RefreshToken 11 | } -------------------------------------------------------------------------------- /oauth2-server-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-core 13 | -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/auth/CallContextBasicAuthenticator.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request.auth 2 | 3 | import nl.myndocs.oauth2.request.CallContext 4 | import nl.myndocs.oauth2.router.RedirectRouter 5 | 6 | object CallContextBasicAuthenticator { 7 | fun handleAuthentication(context: CallContext, router: RedirectRouter) = with(BasicAuthenticator(context)) { 8 | router.route(context, this.extractCredentials()).also { response -> 9 | if (!response.successfulLogin) 10 | openAuthenticationDialog() 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/response/DefaultAccessTokenResponder.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.response 2 | 3 | import nl.myndocs.oauth2.token.AccessToken 4 | 5 | object DefaultAccessTokenResponder : AccessTokenResponder { 6 | override fun createResponse(accessToken: AccessToken): Map = with(accessToken) { 7 | mapOf( 8 | "access_token" to this.accessToken, 9 | "token_type" to this.tokenType, 10 | "expires_in" to this.expiresIn(), 11 | "refresh_token" to this.refreshToken?.refreshToken 12 | ) 13 | } 14 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/auth/BasicAuthenticator.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request.auth 2 | 3 | import nl.myndocs.oauth2.request.CallContext 4 | import nl.myndocs.oauth2.request.headerCaseInsensitive 5 | 6 | open class BasicAuthenticator(protected val context: CallContext) { 7 | fun extractCredentials() = BasicAuth.parseCredentials( 8 | context.headerCaseInsensitive("authorization") ?: "" 9 | ) 10 | 11 | fun openAuthenticationDialog() { 12 | context.respondHeader("WWW-Authenticate", "Basic realm=\"${context.queryParameters["client_id"]}\"") 13 | } 14 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/GrantingCall.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.grant 2 | 3 | import nl.myndocs.oauth2.client.ClientService 4 | import nl.myndocs.oauth2.identity.IdentityService 5 | import nl.myndocs.oauth2.request.CallContext 6 | import nl.myndocs.oauth2.response.AccessTokenResponder 7 | import nl.myndocs.oauth2.token.TokenStore 8 | import nl.myndocs.oauth2.token.converter.Converters 9 | 10 | interface GrantingCall { 11 | val callContext: CallContext 12 | val identityService: IdentityService 13 | val clientService: ClientService 14 | val tokenStore: TokenStore 15 | val converters: Converters 16 | val accessTokenResponder: AccessTokenResponder 17 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/TokenStore.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token 2 | 3 | interface TokenStore { 4 | fun storeAccessToken(accessToken: AccessToken) 5 | 6 | fun accessToken(token: String): AccessToken? 7 | 8 | fun revokeAccessToken(token: String) 9 | 10 | fun storeCodeToken(codeToken: CodeToken) 11 | 12 | fun codeToken(token: String): CodeToken? 13 | 14 | /** 15 | * Retrieve token and delete it from store 16 | */ 17 | fun consumeCodeToken(token: String): CodeToken? 18 | 19 | fun storeRefreshToken(refreshToken: RefreshToken) 20 | 21 | fun refreshToken(token: String): RefreshToken? 22 | 23 | fun revokeRefreshToken(token: String) 24 | } -------------------------------------------------------------------------------- /oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/config/KtorConfiguration.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.ktor.feature.config 2 | 3 | import io.ktor.application.* 4 | import nl.myndocs.oauth2.config.ConfigurationBuilder 5 | import nl.myndocs.oauth2.ktor.feature.request.KtorCallContext 6 | import nl.myndocs.oauth2.request.auth.CallContextBasicAuthenticator 7 | import nl.myndocs.oauth2.router.RedirectRouter 8 | 9 | class KtorConfiguration: ConfigurationBuilder.Configuration() { 10 | var authenticationCallback: (ApplicationCall, RedirectRouter) -> Unit = { call, callRouter -> 11 | val context = KtorCallContext(call) 12 | CallContextBasicAuthenticator.handleAuthentication(context, callRouter) 13 | } 14 | } -------------------------------------------------------------------------------- /oauth2-server-http4k/src/main/java/nl/myndocs/oauth2/http4k/response/ResponseBuilder.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.http4k.response 2 | 3 | import org.http4k.core.Response 4 | import org.http4k.core.Status 5 | 6 | class ResponseBuilder( 7 | var statusCode: Int = 200, 8 | var headers: MutableMap = mutableMapOf(), 9 | var body: String = "" 10 | ) { 11 | 12 | fun build(): Response { 13 | var response = Response( 14 | Status(statusCode, "") 15 | ) 16 | 17 | for (header in headers) { 18 | response = response.header(header.key, header.value) 19 | } 20 | 21 | response = response.body(body) 22 | 23 | return response 24 | } 25 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/converter/UUIDRefreshTokenConverter.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token.converter 2 | 3 | import nl.myndocs.oauth2.identity.Identity 4 | import nl.myndocs.oauth2.token.RefreshToken 5 | import java.time.Instant 6 | import java.util.* 7 | 8 | class UUIDRefreshTokenConverter( 9 | private val refreshTokenExpireInSeconds: Int = 86400 10 | ) : RefreshTokenConverter { 11 | override fun convertToToken(identity: Identity?, clientId: String, requestedScopes: Set): RefreshToken { 12 | return RefreshToken( 13 | UUID.randomUUID().toString(), 14 | Instant.now().plusSeconds(refreshTokenExpireInSeconds.toLong()), 15 | identity, 16 | clientId, 17 | requestedScopes 18 | ) 19 | } 20 | } -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ develop, master ] 9 | pull_request: 10 | branches: [ develop, master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v3 21 | with: 22 | java-version: '11' 23 | distribution: 'temurin' 24 | cache: maven 25 | - name: Build with Maven 26 | run: mvn -B package --file pom.xml 27 | -------------------------------------------------------------------------------- /oauth2-server-sparkjava/src/test/java/nl/myndocs/oauth2/sparkjava/integration/SparkjavaIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.sparkjava.integration 2 | 3 | import nl.myndocs.oauth2.integration.BaseIntegrationTest 4 | import nl.myndocs.oauth2.sparkjava.Oauth2Server 5 | import org.junit.jupiter.api.AfterEach 6 | import org.junit.jupiter.api.BeforeEach 7 | import spark.Spark 8 | 9 | class SparkjavaIntegrationTest : BaseIntegrationTest() { 10 | @BeforeEach 11 | fun before() { 12 | Spark.port(0) 13 | 14 | Oauth2Server.configureOauth2Server{ 15 | configBuilder(this) 16 | } 17 | 18 | Spark.awaitInitialization() 19 | 20 | localPort = Spark.port() 21 | } 22 | 23 | @AfterEach 24 | fun after() { 25 | Spark.stop() 26 | 27 | Spark.awaitStop() 28 | } 29 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/auth/BasicAuth.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request.auth 2 | 3 | import java.util.* 4 | 5 | object BasicAuth { 6 | fun parseCredentials(authorization: String): nl.myndocs.oauth2.authenticator.Credentials { 7 | var username: String? = null 8 | var password: String? = null 9 | 10 | if (authorization.startsWith("basic ", true)) { 11 | val basicAuthorizationString = String(Base64.getDecoder().decode(authorization.substring(6))) 12 | 13 | with(basicAuthorizationString.split(":")) { 14 | if (this.size == 2) { 15 | username = this[0] 16 | password = this[1] 17 | } 18 | } 19 | } 20 | 21 | return nl.myndocs.oauth2.authenticator.Credentials(username, password) 22 | } 23 | } -------------------------------------------------------------------------------- /oauth2-server-javalin/src/test/java/nl/myndocs/oauth2/javalin/integration/JavalinIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.javalin.integration 2 | 3 | import io.javalin.Javalin 4 | import nl.myndocs.oauth2.integration.BaseIntegrationTest 5 | import nl.myndocs.oauth2.javalin.enableOauthServer 6 | import org.junit.jupiter.api.AfterEach 7 | import org.junit.jupiter.api.BeforeEach 8 | 9 | class JavalinIntegrationTest : BaseIntegrationTest() { 10 | 11 | private val server: Javalin = Javalin.create() 12 | .apply { 13 | enableOauthServer { 14 | configBuilder(this) 15 | } 16 | } 17 | 18 | @BeforeEach 19 | fun before() { 20 | server.start(0) 21 | 22 | localPort = server.port() 23 | } 24 | 25 | @AfterEach 26 | fun after() { 27 | server.stop() 28 | } 29 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/converter/UUIDCodeTokenConverter.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token.converter 2 | 3 | import nl.myndocs.oauth2.identity.Identity 4 | import nl.myndocs.oauth2.token.CodeToken 5 | import java.time.Instant 6 | import java.util.* 7 | 8 | class UUIDCodeTokenConverter( 9 | private val codeTokenExpireInSeconds: Int = 300 10 | ) : CodeTokenConverter { 11 | override fun convertToToken( 12 | identity: Identity, 13 | clientId: String, 14 | redirectUri: String, 15 | requestedScopes: Set 16 | ): CodeToken { 17 | return CodeToken( 18 | UUID.randomUUID().toString(), 19 | Instant.now().plusSeconds(codeTokenExpireInSeconds.toLong()), 20 | identity, 21 | clientId, 22 | redirectUri, 23 | requestedScopes 24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/test/java/nl/myndocs/oauth2/scope/ScopeParserTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.scope 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.* 5 | import org.junit.jupiter.api.Test 6 | 7 | internal class ScopeParserTest { 8 | @Test 9 | fun nullShouldResultInEmptySet() { 10 | assertThat( 11 | ScopeParser.parseScopes(null), 12 | `is`(empty()) 13 | ) 14 | } 15 | 16 | @Test 17 | fun emptyStringShouldResultInEmptySet() { 18 | assertThat( 19 | ScopeParser.parseScopes(""), 20 | `is`(empty()) 21 | ) 22 | } 23 | 24 | @Test 25 | fun setShouldBeSeparatedBySpace() { 26 | assertThat( 27 | ScopeParser.parseScopes("foo bar"), 28 | `is`(equalTo(setOf("foo", "bar"))) 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /oauth2-server-client-inmemory/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-client-inmemory 13 | 14 | 15 | 16 | nl.myndocs 17 | oauth2-server-core 18 | ${project.version} 19 | provided 20 | 21 | 22 | -------------------------------------------------------------------------------- /oauth2-server-identity-inmemory/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-identity-inmemory 13 | 14 | 15 | 16 | nl.myndocs 17 | oauth2-server-core 18 | ${project.version} 19 | provided 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /oauth2-server-token-store-inmemory/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-token-store-inmemory 13 | 14 | 15 | 16 | nl.myndocs 17 | oauth2-server-core 18 | ${project.version} 19 | provided 20 | 21 | 22 | -------------------------------------------------------------------------------- /oauth2-server-http4k/src/test/java/nl/myndocs/oauth2/http4k/integration/Http4kIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.http4k.integration 2 | 3 | import nl.myndocs.oauth2.http4k.enableOauth2 4 | import nl.myndocs.oauth2.integration.BaseIntegrationTest 5 | import org.http4k.routing.RoutingHttpHandler 6 | import org.http4k.routing.routes 7 | import org.http4k.server.Jetty 8 | import org.http4k.server.asServer 9 | import org.junit.jupiter.api.AfterEach 10 | import org.junit.jupiter.api.BeforeEach 11 | 12 | class Http4kIntegrationTest : BaseIntegrationTest() { 13 | val server = routes(*emptyArray()) 14 | .let { it.enableOauth2 { configBuilder(this) } } 15 | .let { it.asServer(Jetty(0)) } 16 | 17 | @BeforeEach 18 | fun before() { 19 | server.start() 20 | 21 | localPort = server.port() 22 | } 23 | 24 | @AfterEach 25 | fun after() { 26 | server.stop() 27 | } 28 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/token/converter/UUIDAccessTokenConverter.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.token.converter 2 | 3 | import nl.myndocs.oauth2.identity.Identity 4 | import nl.myndocs.oauth2.token.AccessToken 5 | import nl.myndocs.oauth2.token.RefreshToken 6 | import java.time.Instant 7 | import java.util.* 8 | 9 | class UUIDAccessTokenConverter( 10 | private val accessTokenExpireInSeconds: Int = 3600 11 | ) : AccessTokenConverter { 12 | 13 | override fun convertToToken( 14 | identity: Identity?, 15 | clientId: String, 16 | requestedScopes: Set, 17 | refreshToken: RefreshToken? 18 | ): AccessToken { 19 | return AccessToken( 20 | UUID.randomUUID().toString(), 21 | "bearer", 22 | Instant.now().plusSeconds(accessTokenExpireInSeconds.toLong()), 23 | identity, 24 | clientId, 25 | requestedScopes, 26 | refreshToken 27 | ) 28 | } 29 | } -------------------------------------------------------------------------------- /oauth2-server-jwt/src/main/java/nl/myndocs/convert/DefaultJwtBuilder.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.convert 2 | 3 | import com.auth0.jwt.JWT 4 | import nl.myndocs.oauth2.identity.Identity 5 | import java.time.Instant 6 | import java.util.* 7 | 8 | object DefaultJwtBuilder : JwtBuilder { 9 | override fun buildJwt(identity: Identity?, clientId: String, requestedScopes: Set, expiresInSeconds: Long) = 10 | JWT.create() 11 | .withIssuedAt(Date.from(Instant.now())) 12 | .withExpiresAt( 13 | Date.from( 14 | Instant.now() 15 | .plusSeconds(expiresInSeconds) 16 | ) 17 | ) 18 | .withClaim("client_id", clientId) 19 | .withArrayClaim("scopes", requestedScopes.toTypedArray()) 20 | .let { withBuilder -> if (identity != null) withBuilder.withClaim("username", identity.username) else withBuilder } 21 | } -------------------------------------------------------------------------------- /oauth2-server-jwt/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-jwt 13 | 14 | 15 | 16 | nl.myndocs 17 | oauth2-server-core 18 | ${project.version} 19 | provided 20 | 21 | 22 | com.auth0 23 | java-jwt 24 | 3.5.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /oauth2-server-hexagon/src/test/java/nl/myndocs/oauth2/hexagon/integration/HexagonIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.hexagon.integration 2 | 3 | import com.hexagonkt.http.server.Server 4 | import com.hexagonkt.http.server.ServerPort 5 | import com.hexagonkt.http.server.jetty.JettyServletAdapter 6 | import com.hexagonkt.injection.InjectionManager.bindObject 7 | import nl.myndocs.oauth2.hexagon.enableOauthServer 8 | import nl.myndocs.oauth2.integration.BaseIntegrationTest 9 | import org.junit.jupiter.api.AfterEach 10 | import org.junit.jupiter.api.BeforeEach 11 | 12 | internal class HexagonIntegrationTest : BaseIntegrationTest() { 13 | val server: Server by lazy { 14 | Server { 15 | // @TODO: open random port? 16 | enableOauthServer { configBuilder(this) } 17 | } 18 | } 19 | 20 | @BeforeEach 21 | fun before() { 22 | bindObject(JettyServletAdapter()) 23 | server.start() 24 | 25 | localPort = server.bindPort 26 | } 27 | 28 | @AfterEach 29 | fun after() { 30 | server.stop() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /oauth2-server-client-inmemory/src/main/java/nl/myndocs/oauth2/client/inmemory/InMemoryClient.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.client.inmemory 2 | 3 | import nl.myndocs.oauth2.client.Client 4 | import nl.myndocs.oauth2.client.ClientService 5 | 6 | class InMemoryClient : ClientService { 7 | private val clients = mutableListOf() 8 | 9 | fun client(inline: ClientConfiguration.() -> Unit): InMemoryClient { 10 | val client = ClientConfiguration() 11 | inline(client) 12 | 13 | clients.add(client) 14 | return this 15 | } 16 | 17 | override fun clientOf(clientId: String): Client? { 18 | return clients.filter { it.clientId == clientId } 19 | .map { client -> nl.myndocs.oauth2.client.Client(client.clientId!!, client.scopes, client.redirectUris, client.authorizedGrantTypes) } 20 | .firstOrNull() 21 | } 22 | 23 | override fun validClient(client: Client, clientSecret: String): Boolean { 24 | return configuredClient(client.clientId)!!.clientSecret == clientSecret 25 | } 26 | 27 | private fun configuredClient(clientId: String) = 28 | clients.firstOrNull { it.clientId == clientId } 29 | } -------------------------------------------------------------------------------- /oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtRefreshTokenConverter.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.convert 2 | 3 | import com.auth0.jwt.algorithms.Algorithm 4 | import nl.myndocs.oauth2.identity.Identity 5 | import nl.myndocs.oauth2.token.RefreshToken 6 | import nl.myndocs.oauth2.token.converter.RefreshTokenConverter 7 | import java.time.Instant 8 | 9 | class JwtRefreshTokenConverter( 10 | private val algorithm: Algorithm, 11 | private val refreshTokenExpireInSeconds: Int = 86400, 12 | private val jwtBuilder: JwtBuilder = DefaultJwtBuilder 13 | ) : RefreshTokenConverter { 14 | override fun convertToToken(identity: Identity?, clientId: String, requestedScopes: Set): RefreshToken { 15 | val jwtBuilder = jwtBuilder.buildJwt( 16 | identity, 17 | clientId, 18 | requestedScopes, 19 | refreshTokenExpireInSeconds.toLong() 20 | ) 21 | 22 | return RefreshToken( 23 | jwtBuilder.sign(algorithm), 24 | Instant.now().plusSeconds(refreshTokenExpireInSeconds.toLong()), 25 | identity, 26 | clientId, 27 | requestedScopes 28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /oauth2-server-identity-inmemory/src/main/java/nl/myndocs/oauth2/identity/inmemory/InMemoryIdentity.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.identity.inmemory 2 | 3 | import nl.myndocs.oauth2.client.Client 4 | import nl.myndocs.oauth2.identity.Identity 5 | import nl.myndocs.oauth2.identity.IdentityService 6 | 7 | class InMemoryIdentity : IdentityService { 8 | private val identities = mutableListOf() 9 | 10 | fun identity(inline: IdentityConfiguration.() -> Unit): InMemoryIdentity { 11 | val client = IdentityConfiguration() 12 | inline(client) 13 | 14 | identities.add(client) 15 | return this 16 | } 17 | 18 | override fun identityOf(forClient: Client, username: String): Identity? { 19 | val findConfiguration = findConfiguration(username) ?: return null 20 | return Identity(findConfiguration.username!!) 21 | } 22 | 23 | override fun allowedScopes(forClient: Client, identity: Identity, scopes: Set) = scopes 24 | 25 | override fun validCredentials(forClient: Client, identity: Identity, password: String): Boolean = 26 | findConfiguration(identity.username)!!.password == password 27 | 28 | 29 | private fun findConfiguration(username: String) = identities.firstOrNull { it.username == username } 30 | } -------------------------------------------------------------------------------- /docs/sparkjava.md: -------------------------------------------------------------------------------- 1 | # Spark java 2 | 3 | ## Dependencies 4 | 5 | ### Maven 6 | ```xml 7 | 8 | nl.myndocs 9 | oauth2-server-sparkjava 10 | ${myndocs.oauth.version} 11 | 12 | ``` 13 | 14 | ### Gradle 15 | ```groovy 16 | implementation "nl.myndocs:oauth2-server-sparkjava:$myndocs_oauth_version" 17 | ``` 18 | 19 | ## Implementation 20 | ```kotlin 21 | Oauth2Server.configureOauth2Server { 22 | identityService = InMemoryIdentity() 23 | .identity { 24 | username = "foo" 25 | password = "bar" 26 | } 27 | clientService = InMemoryClient() 28 | .client { 29 | clientId = "testapp" 30 | clientSecret = "testpass" 31 | scopes = setOf("trusted") 32 | redirectUris = setOf("https://localhost:4567/callback") 33 | authorizedGrantTypes = setOf( 34 | AuthorizedGrantType.AUTHORIZATION_CODE, 35 | AuthorizedGrantType.PASSWORD, 36 | AuthorizedGrantType.IMPLICIT, 37 | AuthorizedGrantType.REFRESH_TOKEN 38 | ) 39 | } 40 | tokenStore = InMemoryTokenStore() 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /oauth2-server-jwt/src/main/java/nl/myndocs/convert/JwtAccessTokenConverter.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.convert 2 | 3 | import com.auth0.jwt.algorithms.Algorithm 4 | import nl.myndocs.oauth2.identity.Identity 5 | import nl.myndocs.oauth2.token.AccessToken 6 | import nl.myndocs.oauth2.token.RefreshToken 7 | import nl.myndocs.oauth2.token.converter.AccessTokenConverter 8 | import java.time.Instant 9 | 10 | class JwtAccessTokenConverter( 11 | private val algorithm: Algorithm, 12 | private val accessTokenExpireInSeconds: Int = 3600, 13 | private val jwtBuilder: JwtBuilder = DefaultJwtBuilder 14 | ) : AccessTokenConverter { 15 | override fun convertToToken(identity: Identity?, clientId: String, requestedScopes: Set, refreshToken: RefreshToken?): AccessToken { 16 | val jwtBuilder = jwtBuilder.buildJwt( 17 | identity, 18 | clientId, 19 | requestedScopes, 20 | accessTokenExpireInSeconds.toLong() 21 | ) 22 | 23 | return AccessToken( 24 | jwtBuilder.sign(algorithm), 25 | "bearer", 26 | Instant.now().plusSeconds(accessTokenExpireInSeconds.toLong()), 27 | identity, 28 | clientId, 29 | requestedScopes, 30 | refreshToken 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/request/JavalinCallContext.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.javalin.request 2 | 3 | import io.javalin.http.Context 4 | import nl.myndocs.oauth2.request.CallContext 5 | 6 | class JavalinCallContext(private val context: Context) : CallContext { 7 | override val path: String = context.path() 8 | override val method: String = context.method() 9 | override val headers: Map = context.headerMap() 10 | override val queryParameters: Map = context.queryParamMap() 11 | .mapValues { context.queryParam(it.key) } 12 | .filterValues { it != null } 13 | .mapValues { it.value!! } 14 | 15 | override val formParameters: Map = context.formParamMap() 16 | .mapValues { context.formParam(it.key) } 17 | .filterValues { it != null } 18 | .mapValues { it.value!! } 19 | 20 | override fun respondStatus(statusCode: Int) { 21 | context.status(statusCode) 22 | } 23 | 24 | override fun respondHeader(name: String, value: String) { 25 | context.header(name, value) 26 | } 27 | 28 | override fun respondJson(content: Any) { 29 | context.json(content) 30 | } 31 | 32 | override fun redirect(uri: String) { 33 | context.redirect(uri) 34 | } 35 | } -------------------------------------------------------------------------------- /oauth2-server-javalin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-javalin 13 | 14 | 15 | 16 | nl.myndocs 17 | oauth2-server-core 18 | ${project.version} 19 | provided 20 | 21 | 22 | io.javalin 23 | javalin 24 | 3.12.0 25 | provided 26 | 27 | 28 | 29 | nl.myndocs 30 | oauth2-server-integration-base 31 | ${project.version} 32 | test 33 | 34 | 35 | -------------------------------------------------------------------------------- /oauth2-server-ktor/src/test/java/nl/myndocs/oauth2/ktor/integration/KtorIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.ktor.integration 2 | 3 | import io.ktor.application.install 4 | import io.ktor.server.engine.embeddedServer 5 | import io.ktor.server.netty.Netty 6 | import io.ktor.server.netty.NettyApplicationEngine 7 | import nl.myndocs.oauth2.integration.BaseIntegrationTest 8 | import nl.myndocs.oauth2.ktor.feature.Oauth2ServerFeature 9 | import org.junit.jupiter.api.AfterEach 10 | import org.junit.jupiter.api.BeforeEach 11 | import java.net.BindException 12 | import java.util.concurrent.TimeUnit 13 | 14 | 15 | class KtorIntegrationTest : BaseIntegrationTest() { 16 | 17 | var server: NettyApplicationEngine? = null 18 | 19 | @BeforeEach 20 | fun before() { 21 | for (port in 49152..65535) { 22 | localPort = port 23 | try { 24 | server = embeddedServer(Netty, port = localPort!!) { 25 | install(Oauth2ServerFeature) { 26 | configBuilder(this) 27 | } 28 | } 29 | 30 | server!!.start(false) 31 | break 32 | } catch (e: BindException) { 33 | e.printStackTrace() 34 | } 35 | } 36 | } 37 | 38 | @AfterEach 39 | fun after() { 40 | server!!.stop(0, 10, TimeUnit.SECONDS) 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /oauth2-server-hexagon/src/main/java/nl/myndocs/oauth2/hexagon/Oauth2Server.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.hexagon 2 | 3 | import com.hexagonkt.http.server.Call 4 | import com.hexagonkt.http.server.Router 5 | import nl.myndocs.oauth2.config.ConfigurationBuilder 6 | import nl.myndocs.oauth2.hexagon.request.HexagonCallContext 7 | import nl.myndocs.oauth2.request.auth.CallContextBasicAuthenticator 8 | import nl.myndocs.oauth2.router.RedirectRouter 9 | 10 | 11 | fun Router.enableOauthServer( 12 | authenticationCallback: (Call, RedirectRouter) -> Unit = { call, callRouter -> 13 | val context = HexagonCallContext(call) 14 | CallContextBasicAuthenticator.handleAuthentication(context, callRouter) 15 | }, 16 | configurationCallback: ConfigurationBuilder.Configuration.() -> Unit 17 | ) { 18 | val configuration = ConfigurationBuilder.build(configurationCallback) 19 | 20 | val callRouter = configuration.callRouter 21 | 22 | post(callRouter.tokenEndpoint) { 23 | callRouter.route(HexagonCallContext(this)) 24 | } 25 | 26 | get(callRouter.authorizeEndpoint) { 27 | authenticationCallback(this, callRouter) 28 | } 29 | 30 | post(callRouter.authorizeEndpoint) { 31 | callRouter.route(HexagonCallContext(this)) 32 | } 33 | 34 | get(callRouter.tokenInfoEndpoint) { 35 | callRouter.route(HexagonCallContext(this)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/javalin.md: -------------------------------------------------------------------------------- 1 | # Javalin 2 | 3 | ## Dependencies 4 | 5 | ### Maven 6 | ```xml 7 | 8 | nl.myndocs 9 | oauth2-server-javalin 10 | ${myndocs.oauth.version} 11 | 12 | ``` 13 | 14 | ### Gradle 15 | ```groovy 16 | implementation "nl.myndocs:oauth2-server-javalin:$myndocs_oauth_version" 17 | ``` 18 | 19 | ## Implementation 20 | ```kotlin 21 | Javalin.create().apply { 22 | enableOauthServer { 23 | identityService = InMemoryIdentity() 24 | .identity { 25 | username = "foo" 26 | password = "bar" 27 | } 28 | clientService = InMemoryClient() 29 | .client { 30 | clientId = "testapp" 31 | clientSecret = "testpass" 32 | scopes = setOf("trusted") 33 | redirectUris = setOf("https://localhost:7000/callback") 34 | authorizedGrantTypes = setOf( 35 | AuthorizedGrantType.AUTHORIZATION_CODE, 36 | AuthorizedGrantType.PASSWORD, 37 | AuthorizedGrantType.IMPLICIT, 38 | AuthorizedGrantType.REFRESH_TOKEN 39 | ) 40 | } 41 | tokenStore = InMemoryTokenStore() 42 | } 43 | }.start(7000) 44 | ``` 45 | -------------------------------------------------------------------------------- /oauth2-server-sparkjava/src/main/java/nl/myndocs/oauth2/sparkjava/request/SparkjavaCallContext.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.sparkjava.request 2 | 3 | import nl.myndocs.oauth2.authenticator.Credentials 4 | import nl.myndocs.oauth2.json.JsonMapper 5 | import nl.myndocs.oauth2.request.CallContext 6 | import spark.Request 7 | import spark.Response 8 | 9 | class SparkjavaCallContext(val request: Request, val response: Response) : CallContext { 10 | override val path: String = request.pathInfo() 11 | override val method: String = request.requestMethod() 12 | override val headers: Map = request.headers() 13 | .map { it.toLowerCase() to request.headers(it) } 14 | .toMap() 15 | 16 | override val queryParameters: Map = request.queryParams() 17 | .map { it.toLowerCase() to request.queryParams(it) } 18 | .toMap() 19 | 20 | override val formParameters: Map = queryParameters 21 | 22 | override fun respondStatus(statusCode: Int) { 23 | response.status(statusCode) 24 | response.body("") 25 | } 26 | 27 | override fun respondHeader(name: String, value: String) { 28 | response.header(name, value) 29 | } 30 | 31 | override fun respondJson(content: Any) { 32 | response.body(JsonMapper.toJson(content)) 33 | } 34 | 35 | override fun redirect(uri: String) { 36 | response.redirect(uri) 37 | } 38 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/CallRouterBuilder.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.config 2 | 3 | import nl.myndocs.oauth2.CallRouter 4 | import nl.myndocs.oauth2.grant.* 5 | import nl.myndocs.oauth2.identity.TokenInfo 6 | import nl.myndocs.oauth2.request.CallContext 7 | 8 | internal object CallRouterBuilder { 9 | class Configuration { 10 | var tokenEndpoint: String = "/oauth/token" 11 | var authorizeEndpoint: String = "/oauth/authorize" 12 | var tokenInfoEndpoint: String = "/oauth/tokeninfo" 13 | var tokenInfoCallback: (TokenInfo) -> Map = { tokenInfo -> 14 | mapOf( 15 | "username" to tokenInfo.identity?.username, 16 | "scopes" to tokenInfo.scopes 17 | ).filterValues { it != null } 18 | } 19 | var granters: List Granter> = listOf() 20 | } 21 | 22 | fun build(configuration: Configuration, grantingCallFactory: (CallContext) -> GrantingCall) = CallRouter( 23 | configuration.tokenEndpoint, 24 | configuration.authorizeEndpoint, 25 | configuration.tokenInfoEndpoint, 26 | configuration.tokenInfoCallback, 27 | listOf Granter>( 28 | { grantPassword() }, 29 | { grantAuthorizationCode() }, 30 | { grantClientCredentials() }, 31 | { grantRefreshToken() } 32 | ) + configuration.granters, 33 | grantingCallFactory 34 | ) 35 | } -------------------------------------------------------------------------------- /docs/ktor.md: -------------------------------------------------------------------------------- 1 | # Ktor 2 | 3 | ## Dependencies 4 | 5 | ### Maven 6 | ```xml 7 | 8 | nl.myndocs 9 | oauth2-server-ktor 10 | ${myndocs.oauth.version} 11 | 12 | ``` 13 | 14 | ### Gradle 15 | ```groovy 16 | implementation "nl.myndocs:oauth2-server-ktor:$myndocs_oauth_version" 17 | ``` 18 | 19 | ## Implementation 20 | ```kotlin 21 | embeddedServer(Netty, 8080) { 22 | install(Oauth2ServerFeature) { 23 | identityService = InMemoryIdentity() 24 | .identity { 25 | username = "foo" 26 | password = "bar" 27 | } 28 | clientService = InMemoryClient() 29 | .client { 30 | clientId = "testapp" 31 | clientSecret = "testpass" 32 | scopes = setOf("trusted") 33 | redirectUris = setOf("https://localhost:8080/callback") 34 | authorizedGrantTypes = setOf( 35 | AuthorizedGrantType.AUTHORIZATION_CODE, 36 | AuthorizedGrantType.PASSWORD, 37 | AuthorizedGrantType.IMPLICIT, 38 | AuthorizedGrantType.REFRESH_TOKEN 39 | ) 40 | } 41 | tokenStore = InMemoryTokenStore() 42 | } 43 | }.start(wait = true) 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/http4k.md: -------------------------------------------------------------------------------- 1 | # http4k 2 | 3 | ## Dependencies 4 | 5 | ### Maven 6 | ```xml 7 | 8 | nl.myndocs 9 | oauth2-server-http4k 10 | ${myndocs.oauth.version} 11 | 12 | ``` 13 | 14 | ### Gradle 15 | ```groovy 16 | implementation "nl.myndocs:oauth2-server-http4k:$myndocs_oauth_version" 17 | ``` 18 | 19 | 20 | ## Implementation 21 | ```kotlin 22 | val app: HttpHandler = routes( 23 | "/ping" bind GET to { _: Request -> Response(Status.OK).body("pong!") } 24 | ).enableOauth2 { 25 | identityService = InMemoryIdentity() 26 | .identity { 27 | username = "foo" 28 | password = "bar" 29 | } 30 | clientService = InMemoryClient() 31 | .client { 32 | clientId = "testapp" 33 | clientSecret = "testpass" 34 | scopes = setOf("trusted") 35 | redirectUris = setOf("http://localhost:8080/callback") 36 | authorizedGrantTypes = setOf( 37 | AuthorizedGrantType.AUTHORIZATION_CODE, 38 | AuthorizedGrantType.PASSWORD, 39 | AuthorizedGrantType.IMPLICIT, 40 | AuthorizedGrantType.REFRESH_TOKEN 41 | ) 42 | } 43 | tokenStore = InMemoryTokenStore() 44 | } 45 | 46 | app.asServer(Jetty(9000)).start() 47 | ``` 48 | -------------------------------------------------------------------------------- /oauth2-server-sparkjava/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-sparkjava 13 | 14 | 15 | 16 | com.sparkjava 17 | spark-core 18 | 2.8.0 19 | provided 20 | 21 | 22 | nl.myndocs 23 | oauth2-server-core 24 | ${project.version} 25 | provided 26 | 27 | 28 | nl.myndocs 29 | oauth2-server-json 30 | ${project.version} 31 | shaded 32 | 33 | 34 | 35 | nl.myndocs 36 | oauth2-server-integration-base 37 | ${project.version} 38 | test 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /oauth2-server-core/src/test/java/nl/myndocs/oauth2/request/auth/BasicAuthenticatorTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.request.auth 2 | 3 | import io.mockk.every 4 | import io.mockk.mockk 5 | import nl.myndocs.oauth2.request.CallContext 6 | import org.hamcrest.CoreMatchers.* 7 | import org.hamcrest.MatcherAssert.assertThat 8 | import org.junit.jupiter.api.Test 9 | import java.util.* 10 | 11 | internal class BasicAuthenticatorTest { 12 | 13 | @Test 14 | fun `test authorization head is case insensitive with all uppercase input`() { 15 | `test authorization head is case insensitive with input`( 16 | "AUTHORIZATION" 17 | ) 18 | } 19 | 20 | @Test 21 | fun `test authorization head is case insensitive with all lowercase input`() { 22 | `test authorization head is case insensitive with input`( 23 | "authorization" 24 | ) 25 | } 26 | 27 | private fun `test authorization head is case insensitive with input`(authorizationKeyName: String) { 28 | val callContext = mockk() 29 | val username = "test" 30 | val password = "test-password" 31 | 32 | val testCredentials = Base64.getEncoder().encodeToString("$username:$password".toByteArray()) 33 | 34 | every { callContext.headers } returns mapOf(authorizationKeyName to "basic $testCredentials") 35 | val credentials = BasicAuthenticator(callContext) 36 | .extractCredentials() 37 | 38 | assertThat(credentials, `is`(notNullValue())) 39 | assertThat(credentials!!.username, `is`(equalTo(username))) 40 | assertThat(credentials.password, `is`(equalTo(password))) 41 | } 42 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterRefresh.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.grant 2 | 3 | import nl.myndocs.oauth2.client.AuthorizedGrantType 4 | import nl.myndocs.oauth2.exception.InvalidClientException 5 | import nl.myndocs.oauth2.exception.InvalidGrantException 6 | import nl.myndocs.oauth2.exception.InvalidRequestException 7 | import nl.myndocs.oauth2.request.RefreshTokenRequest 8 | import nl.myndocs.oauth2.token.AccessToken 9 | 10 | fun GrantingCall.refresh(refreshTokenRequest: RefreshTokenRequest): AccessToken { 11 | throwExceptionIfUnverifiedClient(refreshTokenRequest) 12 | 13 | if (refreshTokenRequest.refreshToken == null) { 14 | throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("refresh_token")) 15 | } 16 | 17 | val refreshToken = tokenStore.refreshToken(refreshTokenRequest.refreshToken) ?: throw InvalidGrantException() 18 | 19 | if (refreshToken.clientId != refreshTokenRequest.clientId) { 20 | throw InvalidGrantException() 21 | } 22 | 23 | val client = clientService.clientOf(refreshToken.clientId) ?: throw InvalidClientException() 24 | 25 | val authorizedGrantType = AuthorizedGrantType.REFRESH_TOKEN 26 | if (!client.authorizedGrantTypes.contains(authorizedGrantType)) { 27 | throw InvalidGrantException("Authorize not allowed: '$authorizedGrantType'") 28 | } 29 | 30 | val accessToken = converters.accessTokenConverter.convertToToken( 31 | refreshToken.identity, 32 | refreshToken.clientId, 33 | refreshToken.scopes, 34 | converters.refreshTokenConverter.convertToToken(refreshToken) 35 | ) 36 | 37 | tokenStore.storeAccessToken(accessToken) 38 | 39 | return accessToken 40 | } 41 | -------------------------------------------------------------------------------- /oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/Oauth2Server.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.javalin 2 | 3 | import io.javalin.http.Context 4 | import io.javalin.Javalin 5 | import io.javalin.apibuilder.ApiBuilder.* 6 | import nl.myndocs.oauth2.config.ConfigurationBuilder 7 | import nl.myndocs.oauth2.javalin.request.JavalinCallContext 8 | import nl.myndocs.oauth2.request.auth.CallContextBasicAuthenticator 9 | import nl.myndocs.oauth2.router.RedirectRouter 10 | 11 | 12 | fun Javalin.enableOauthServer( 13 | authenticationCallback: (Context, RedirectRouter) -> Unit = { ctx, callRouter -> 14 | val context = JavalinCallContext(ctx) 15 | CallContextBasicAuthenticator.handleAuthentication(context, callRouter) 16 | }, 17 | configurationCallback: ConfigurationBuilder.Configuration.() -> Unit 18 | ) { 19 | val configuration = ConfigurationBuilder.build(configurationCallback) 20 | 21 | val callRouter = configuration.callRouter 22 | 23 | this.routes { 24 | path(callRouter.tokenEndpoint) { 25 | post { ctx -> 26 | val javalinCallContext = JavalinCallContext(ctx) 27 | callRouter.route(javalinCallContext) 28 | } 29 | } 30 | 31 | path(callRouter.authorizeEndpoint) { 32 | get { ctx -> 33 | authenticationCallback(ctx, callRouter) 34 | } 35 | 36 | post { ctx -> 37 | authenticationCallback(ctx, callRouter) 38 | } 39 | } 40 | 41 | path(callRouter.tokenInfoEndpoint) { 42 | get { ctx -> 43 | val javalinCallContext = JavalinCallContext(ctx) 44 | callRouter.route(javalinCallContext) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /oauth2-server-hexagon/src/main/java/nl/myndocs/oauth2/hexagon/request/HexagonCallContext.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.hexagon.request 2 | 3 | import com.hexagonkt.http.server.Call 4 | import nl.myndocs.oauth2.json.JsonMapper 5 | import nl.myndocs.oauth2.request.CallContext 6 | import java.net.URLDecoder 7 | import java.nio.charset.StandardCharsets 8 | 9 | class HexagonCallContext(val call: Call) : CallContext { 10 | override val path: String = call.request.path 11 | override val method: String = call.request.method.name 12 | override val headers: Map = call.request.headers 13 | .mapValues { it.value.joinToString(";") } 14 | 15 | override val queryParameters: Map = (call.request 16 | .runCatching { queryString } 17 | .getOrNull() ?: "") 18 | .split("&") 19 | .filter { it.contains("=") } 20 | .associate { 21 | val (key, value) = it.split("=") 22 | Pair( 23 | key.toLowerCase(), 24 | URLDecoder.decode(value, StandardCharsets.UTF_8.name()) 25 | ) 26 | } 27 | 28 | override val formParameters: Map = call.parameters 29 | .mapValues { it.value.lastOrNull() } 30 | .filterValues { it != null } 31 | .mapValues { it.value!! } 32 | 33 | override fun respondStatus(statusCode: Int) { 34 | call.response.status = statusCode 35 | } 36 | 37 | override fun respondHeader(name: String, value: String) { 38 | call.response.setHeader(name, value) 39 | } 40 | 41 | override fun respondJson(content: Any) { 42 | call.response.body = JsonMapper.toJson(content) 43 | } 44 | 45 | override fun redirect(uri: String) { 46 | call.redirect(uri) 47 | } 48 | } -------------------------------------------------------------------------------- /oauth2-server-http4k/src/main/java/nl/myndocs/oauth2/http4k/request/Http4kCallContext.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.http4k.request 2 | 3 | import nl.myndocs.oauth2.http4k.response.ResponseBuilder 4 | import nl.myndocs.oauth2.json.JsonMapper 5 | import nl.myndocs.oauth2.request.CallContext 6 | import org.http4k.core.Request 7 | import org.http4k.core.body.form 8 | import org.http4k.core.queries 9 | 10 | class Http4kCallContext(val request: Request, val responseBuilder: ResponseBuilder) : CallContext { 11 | override val path: String = request.uri.path 12 | override val method: String = request.method.name 13 | override val headers: Map = request.headers 14 | .toMap() 15 | .filterValues { it != null } 16 | .map { it.key.toLowerCase() to it.value!! } 17 | .toMap() 18 | override val queryParameters: Map = request.uri.queries() 19 | .toMap() 20 | .filterValues { it != null } 21 | .mapValues { it.value!! } 22 | .map { it.key.toLowerCase() to it.value } 23 | .toMap() 24 | 25 | override val formParameters: Map = request.form() 26 | .toMap() 27 | .filterValues { it != null } 28 | .mapValues { it.value!! } 29 | .map { it.key.toLowerCase() to it.value } 30 | .toMap() 31 | 32 | override fun respondStatus(statusCode: Int) { 33 | responseBuilder.statusCode = statusCode 34 | } 35 | 36 | override fun respondHeader(name: String, value: String) { 37 | responseBuilder.headers[name] = value 38 | } 39 | 40 | override fun respondJson(content: Any) { 41 | responseBuilder.body = JsonMapper.toJson(content) 42 | } 43 | 44 | override fun redirect(uri: String) { 45 | responseBuilder.statusCode = 302 46 | responseBuilder.headers["Location"] = uri 47 | } 48 | } -------------------------------------------------------------------------------- /oauth2-server-sparkjava/src/main/java/nl/myndocs/oauth2/sparkjava/Oauth2Server.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.sparkjava 2 | 3 | import nl.myndocs.oauth2.config.ConfigurationBuilder 4 | import nl.myndocs.oauth2.request.auth.BasicAuthenticator 5 | import nl.myndocs.oauth2.request.auth.CallContextBasicAuthenticator 6 | import nl.myndocs.oauth2.router.RedirectRouter 7 | import nl.myndocs.oauth2.sparkjava.request.SparkjavaCallContext 8 | import spark.Request 9 | import spark.Response 10 | import spark.Spark.get 11 | import spark.Spark.post 12 | 13 | object Oauth2Server { 14 | fun configureOauth2Server( 15 | authenticationCallback: (Request, Response, RedirectRouter) -> Unit = { request, response, callRouter -> 16 | val context = SparkjavaCallContext(request, response) 17 | CallContextBasicAuthenticator.handleAuthentication(context, callRouter) 18 | }, 19 | configurationCallback: ConfigurationBuilder.Configuration.() -> Unit 20 | ) { 21 | val configuration = ConfigurationBuilder.build(configurationCallback) 22 | 23 | val callRouter = configuration.callRouter 24 | 25 | post(callRouter.tokenEndpoint) { req, res -> 26 | val sparkjavaCallContext = SparkjavaCallContext(req, res) 27 | callRouter.route(sparkjavaCallContext) 28 | 29 | res.body() 30 | } 31 | 32 | 33 | get(callRouter.authorizeEndpoint) { req, res -> 34 | authenticationCallback(req, res, callRouter) 35 | 36 | res.body() 37 | } 38 | 39 | post(callRouter.authorizeEndpoint) { req, res -> 40 | authenticationCallback(req, res, callRouter) 41 | 42 | res.body() 43 | } 44 | 45 | get(callRouter.tokenInfoEndpoint) { req, res -> 46 | val sparkjavaCallContext = SparkjavaCallContext(req, res) 47 | callRouter.route(sparkjavaCallContext) 48 | 49 | res.body() 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /oauth2-server-token-store-inmemory/src/main/java/nl/myndocs/oauth2/tokenstore/inmemory/InMemoryTokenStore.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.tokenstore.inmemory 2 | 3 | import nl.myndocs.oauth2.token.* 4 | 5 | class InMemoryTokenStore : TokenStore { 6 | private val accessTokens = mutableMapOf() 7 | private val codes = mutableMapOf() 8 | private val refreshTokens = mutableMapOf() 9 | 10 | override fun storeAccessToken(accessToken: AccessToken) { 11 | accessTokens[accessToken.accessToken] = accessToken 12 | 13 | if (accessToken.refreshToken != null) { 14 | storeRefreshToken(accessToken.refreshToken!!) 15 | } 16 | } 17 | 18 | override fun accessToken(token: String): AccessToken? = 19 | locateToken(accessTokens, token) 20 | 21 | override fun storeCodeToken(codeToken: CodeToken) { 22 | codes[codeToken.codeToken] = codeToken 23 | } 24 | 25 | override fun codeToken(token: String): CodeToken? = 26 | locateToken(codes, token) 27 | 28 | override fun consumeCodeToken(token: String): CodeToken? = codes.remove(token) 29 | 30 | override fun storeRefreshToken(refreshToken: RefreshToken) { 31 | refreshTokens[refreshToken.refreshToken] = refreshToken 32 | } 33 | 34 | override fun refreshToken(token: String): RefreshToken? = 35 | locateToken(refreshTokens, token) 36 | 37 | private fun locateToken(tokens: MutableMap, token: String): T? { 38 | var tokenFromMap = tokens[token] 39 | 40 | if (tokenFromMap != null && tokenFromMap.expired()) { 41 | tokens.remove(token) 42 | 43 | tokenFromMap = null 44 | } 45 | 46 | return tokenFromMap 47 | } 48 | 49 | override fun revokeAccessToken(token: String) { 50 | accessTokens.remove(token) 51 | } 52 | 53 | override fun revokeRefreshToken(token: String) { 54 | refreshTokens.remove(token) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /oauth2-server-http4k/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-http4k 13 | 14 | 15 | 16 | org.http4k 17 | http4k-core 18 | 3.37.1 19 | provided 20 | 21 | 22 | org.http4k 23 | http4k-server-jetty 24 | 3.37.1 25 | test 26 | 27 | 28 | nl.myndocs 29 | oauth2-server-core 30 | ${project.version} 31 | provided 32 | 33 | 34 | nl.myndocs 35 | oauth2-server-json 36 | ${project.version} 37 | shaded 38 | 39 | 40 | 41 | nl.myndocs 42 | oauth2-server-integration-base 43 | ${project.version} 44 | test 45 | 46 | 47 | 48 | 49 | 50 | http4k 51 | http://dl.bintray.com/http4k 52 | 53 | 54 | -------------------------------------------------------------------------------- /oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/Oauth2ServerFeature.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.ktor.feature 2 | 3 | import io.ktor.application.ApplicationCallPipeline 4 | import io.ktor.application.ApplicationFeature 5 | import io.ktor.application.call 6 | import io.ktor.util.AttributeKey 7 | import nl.myndocs.oauth2.config.Configuration 8 | import nl.myndocs.oauth2.config.ConfigurationBuilder 9 | import nl.myndocs.oauth2.ktor.feature.config.KtorConfiguration 10 | import nl.myndocs.oauth2.ktor.feature.request.KtorCallContext 11 | 12 | class Oauth2ServerFeature(configuration: Configuration) { 13 | val callRouter = configuration.callRouter 14 | 15 | companion object Feature : ApplicationFeature { 16 | override val key = AttributeKey("Oauth2ServerFeature") 17 | 18 | override fun install(pipeline: ApplicationCallPipeline, configure: KtorConfiguration.() -> Unit): Oauth2ServerFeature { 19 | val ktorConfiguration = KtorConfiguration() 20 | configure(ktorConfiguration) 21 | val configuration = ConfigurationBuilder.build(configure as ConfigurationBuilder.Configuration.() -> Unit, ktorConfiguration) 22 | 23 | 24 | val feature = Oauth2ServerFeature(configuration) 25 | 26 | pipeline.intercept(ApplicationCallPipeline.Features) { 27 | val ktorCallContext = KtorCallContext(call) 28 | 29 | if (configuration.callRouter.authorizeEndpoint == ktorCallContext.path) { 30 | ktorConfiguration.authenticationCallback(call, feature.callRouter) 31 | } 32 | 33 | if ( 34 | arrayOf( 35 | configuration.callRouter.tokenEndpoint, 36 | configuration.callRouter.tokenInfoEndpoint 37 | ).contains(ktorCallContext.path) 38 | ) { 39 | feature.callRouter.route(ktorCallContext) 40 | } 41 | } 42 | 43 | return feature 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /oauth2-server-hexagon/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-hexagon 13 | 14 | 15 | 1.0.0 16 | 17 | 18 | 19 | nl.myndocs 20 | oauth2-server-core 21 | ${project.version} 22 | provided 23 | 24 | 25 | com.hexagonkt 26 | port_http_server 27 | ${hexagon.version} 28 | provided 29 | 30 | 31 | nl.myndocs 32 | oauth2-server-json 33 | ${project.version} 34 | shaded 35 | 36 | 37 | com.hexagonkt 38 | http_server_jetty 39 | ${hexagon.version} 40 | test 41 | 42 | 43 | 44 | 45 | nl.myndocs 46 | oauth2-server-integration-base 47 | ${project.version} 48 | test 49 | 50 | 51 | 52 | 53 | 54 | jcenter 55 | https://jcenter.bintray.com/ 56 | 57 | 58 | -------------------------------------------------------------------------------- /oauth2-server-http4k/src/main/java/nl/myndocs/oauth2/http4k/Oauth2Server.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.http4k 2 | 3 | import nl.myndocs.oauth2.config.ConfigurationBuilder 4 | import nl.myndocs.oauth2.http4k.request.Http4kCallContext 5 | import nl.myndocs.oauth2.http4k.response.ResponseBuilder 6 | import nl.myndocs.oauth2.request.auth.CallContextBasicAuthenticator 7 | import nl.myndocs.oauth2.router.RedirectRouter 8 | import org.http4k.core.Method 9 | import org.http4k.core.Request 10 | import org.http4k.core.Response 11 | import org.http4k.routing.RoutingHttpHandler 12 | import org.http4k.routing.bind 13 | import org.http4k.routing.routes 14 | 15 | fun RoutingHttpHandler.enableOauth2( 16 | authenticationCallback: (Request, RedirectRouter) -> Response = { request, callRouter -> 17 | val responseBuilder = ResponseBuilder() 18 | val callContext = Http4kCallContext(request, responseBuilder) 19 | 20 | CallContextBasicAuthenticator.handleAuthentication(callContext, callRouter) 21 | 22 | responseBuilder.build() 23 | }, 24 | configurationCallback: ConfigurationBuilder.Configuration.() -> Unit 25 | ): RoutingHttpHandler { 26 | val configuration = ConfigurationBuilder.build(configurationCallback) 27 | 28 | val callRouter = configuration.callRouter 29 | 30 | return routes( 31 | this, 32 | callRouter.tokenEndpoint bind Method.POST to { request: Request -> 33 | val responseBuilder = ResponseBuilder() 34 | val callContext = Http4kCallContext(request, responseBuilder) 35 | callRouter.route(callContext) 36 | 37 | responseBuilder.build() 38 | }, 39 | callRouter.authorizeEndpoint bind Method.GET to { request: Request -> 40 | authenticationCallback(request, callRouter) 41 | }, 42 | callRouter.tokenInfoEndpoint bind Method.GET to { request: Request -> 43 | val responseBuilder = ResponseBuilder() 44 | val callContext = Http4kCallContext(request, responseBuilder) 45 | callRouter.route(callContext) 46 | 47 | responseBuilder.build() 48 | } 49 | ) 50 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/exception/OauthException.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.exception 2 | 3 | open class OauthException(val error: OauthError, val errorDescription: String? = null) : Exception() 4 | 5 | enum class OauthError(val errorName: String) { 6 | /** 7 | * The request is missing a required parameter, includes an 8 | * unsupported parameter value (other than grant type), 9 | * repeats a parameter, includes multiple credentials, 10 | * utilizes more than one mechanism for authenticating the 11 | * client, or is otherwise malformed. 12 | */ 13 | INVALID_REQUEST("invalid_request"), 14 | 15 | /** 16 | * Client authentication failed (e.g., unknown client, no 17 | * client authentication included, or unsupported 18 | * authentication method). The authorization server MAY 19 | * return an HTTP 401 (Unauthorized) status code to indicate 20 | * which HTTP authentication schemes are supported. If the 21 | * client attempted to authenticate via the "Authorization" 22 | * request header field, the authorization server MUST 23 | * respond with an HTTP 401 (Unauthorized) status code and 24 | * include the "WWW-Authenticate" response header field 25 | * matching the authentication scheme used by the client. 26 | */ 27 | INVALID_CLIENT("invalid_client"), 28 | 29 | /** 30 | * The provided authorization grant (e.g., authorization 31 | * code, resource owner credentials) or refresh token is 32 | * invalid, expired, revoked, does not match the redirection 33 | * URI used in the authorization request, or was issued to 34 | * another client. 35 | */ 36 | INVALID_GRANT("invalid_grant"), 37 | 38 | /** 39 | * The authenticated client is not authorized to use this 40 | * authorization grant type. 41 | */ 42 | UNAUTHORIZED_CLIENT("unauthorized_client"), 43 | 44 | /** 45 | * The authorization grant type is not supported by the 46 | * authorization server. 47 | */ 48 | UNSUPPORTED_GRANT_TYPE("unsupported_grant_type"), 49 | 50 | /** 51 | * The requested scope is invalid, unknown, malformed, or 52 | * exceeds the scope granted by the resource owner. 53 | */ 54 | INVALID_SCOPE("invalid_scope") 55 | } -------------------------------------------------------------------------------- /oauth2-server-ktor/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-ktor 13 | 14 | 15 | 1.1.2 16 | 17 | 18 | 19 | 20 | io.ktor 21 | ktor-server-core 22 | ${ktor.version} 23 | provided 24 | 25 | 26 | nl.myndocs 27 | oauth2-server-core 28 | ${project.version} 29 | provided 30 | 31 | 32 | nl.myndocs 33 | oauth2-server-json 34 | ${project.version} 35 | shaded 36 | 37 | 38 | 39 | io.ktor 40 | ktor-server-netty 41 | ${ktor.version} 42 | test 43 | 44 | 45 | nl.myndocs 46 | oauth2-server-integration-base 47 | ${project.version} 48 | test 49 | 50 | 51 | 52 | 53 | 54 | ktor 55 | http://dl.bintray.com/kotlin/ktor 56 | 57 | 58 | kotlinx 59 | http://dl.bintray.com/kotlin/kotlinx 60 | 61 | 62 | jcenter 63 | http://jcenter.bintray.com 64 | 65 | 66 | -------------------------------------------------------------------------------- /oauth2-server-json/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-json 13 | 14 | 15 | 16 | com.google.code.gson 17 | gson 18 | 2.8.9 19 | 20 | 21 | 22 | 23 | 24 | 25 | org.apache.maven.plugins 26 | maven-shade-plugin 27 | 3.1.1 28 | 29 | true 30 | 31 | 32 | com.google.code.gson:* 33 | 34 | 35 | 36 | 37 | com.google.gson 38 | nl.myndocs.oauth2.shaded.com.google.gson 39 | 40 | 41 | true 42 | 43 | 45 | 46 | 47 | 48 | 49 | package 50 | 51 | shade 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/request/KtorCallContext.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.ktor.feature.request 2 | 3 | import io.ktor.application.ApplicationCall 4 | import io.ktor.http.HttpStatusCode 5 | import io.ktor.request.header 6 | import io.ktor.request.httpMethod 7 | import io.ktor.request.path 8 | import io.ktor.request.receiveParameters 9 | import io.ktor.response.header 10 | import io.ktor.response.respondRedirect 11 | import io.ktor.response.respondText 12 | import io.ktor.util.toMap 13 | import kotlinx.coroutines.runBlocking 14 | import nl.myndocs.oauth2.authenticator.Credentials 15 | import nl.myndocs.oauth2.json.JsonMapper 16 | import nl.myndocs.oauth2.request.CallContext 17 | 18 | class KtorCallContext(val applicationCall: ApplicationCall) : CallContext { 19 | override val path: String = applicationCall.request.path() 20 | override val method: String = applicationCall.request.httpMethod.value 21 | override val headers: Map = applicationCall.request 22 | .headers 23 | .toMap() 24 | .mapValues { applicationCall.request.header(it.key) } 25 | .filterValues { it != null } 26 | .mapValues { it.value!! } 27 | 28 | override val queryParameters: Map = applicationCall.request 29 | .queryParameters 30 | .toMap() 31 | .filterValues { it.isNotEmpty() } 32 | .mapValues { it.value.first() } 33 | 34 | private var _formParameters: Map? = null 35 | override val formParameters: Map 36 | get() = receiveParameters() 37 | 38 | private fun receiveParameters(): Map { 39 | if (_formParameters == null) { 40 | _formParameters = runBlocking { 41 | applicationCall.receiveParameters() 42 | .toMap() 43 | .filterValues { it.isNotEmpty() } 44 | .mapValues { it.value.first() } 45 | } 46 | } 47 | 48 | return _formParameters!! 49 | } 50 | 51 | override fun respondStatus(statusCode: Int) { 52 | applicationCall.response.status(HttpStatusCode.fromValue(statusCode)) 53 | } 54 | 55 | override fun respondHeader(name: String, value: String) { 56 | applicationCall.response.header(name, value) 57 | } 58 | 59 | override fun respondJson(content: Any) { 60 | runBlocking { 61 | applicationCall.respondText( 62 | JsonMapper.toJson(content), 63 | io.ktor.http.ContentType.Application.Json 64 | ) 65 | } 66 | } 67 | 68 | override fun redirect(uri: String) { 69 | runBlocking { 70 | applicationCall.respondRedirect(uri) 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /oauth2-server-integration-base/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | kotlin-oauth2-server 7 | nl.myndocs 8 | 0.7.1 9 | 10 | 4.0.0 11 | 12 | oauth2-server-integration-base 13 | 14 | 15 | 2.11.4 16 | 2.11.4 17 | 18 | 19 | 20 | 21 | com.squareup.okhttp3 22 | okhttp 23 | 3.12.1 24 | 25 | 26 | 27 | org.junit.jupiter 28 | junit-jupiter-engine 29 | 5.2.0 30 | provided 31 | 32 | 33 | 34 | 35 | nl.myndocs 36 | oauth2-server-client-inmemory 37 | ${project.version} 38 | 39 | 40 | nl.myndocs 41 | oauth2-server-identity-inmemory 42 | ${project.version} 43 | 44 | 45 | nl.myndocs 46 | oauth2-server-token-store-inmemory 47 | ${project.version} 48 | 49 | 50 | nl.myndocs 51 | oauth2-server-core 52 | ${project.version} 53 | 54 | 55 | 56 | 57 | com.fasterxml.jackson.core 58 | jackson-core 59 | ${jackson.version} 60 | 61 | 62 | com.fasterxml.jackson.core 63 | jackson-databind 64 | ${jackson.databind.version} 65 | 66 | 67 | com.fasterxml.jackson.module 68 | jackson-module-kotlin 69 | ${jackson.version} 70 | 71 | 72 | org.hamcrest 73 | hamcrest-all 74 | 1.3 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/config/ConfigurationBuilder.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.config 2 | 3 | import nl.myndocs.oauth2.client.ClientService 4 | import nl.myndocs.oauth2.grant.Granter 5 | import nl.myndocs.oauth2.grant.GrantingCall 6 | import nl.myndocs.oauth2.identity.IdentityService 7 | import nl.myndocs.oauth2.identity.TokenInfo 8 | import nl.myndocs.oauth2.request.CallContext 9 | import nl.myndocs.oauth2.response.AccessTokenResponder 10 | import nl.myndocs.oauth2.response.DefaultAccessTokenResponder 11 | import nl.myndocs.oauth2.token.TokenStore 12 | import nl.myndocs.oauth2.token.converter.* 13 | 14 | object ConfigurationBuilder { 15 | open class Configuration { 16 | internal val callRouterConfiguration = CallRouterBuilder.Configuration() 17 | 18 | var authorizationEndpoint: String 19 | get() = callRouterConfiguration.authorizeEndpoint 20 | set(value) { 21 | callRouterConfiguration.authorizeEndpoint = value 22 | } 23 | 24 | var tokenEndpoint: String 25 | get() = callRouterConfiguration.tokenEndpoint 26 | set(value) { 27 | callRouterConfiguration.tokenEndpoint = value 28 | } 29 | 30 | var tokenInfoEndpoint: String 31 | get() = callRouterConfiguration.tokenInfoEndpoint 32 | set(value) { 33 | callRouterConfiguration.tokenInfoEndpoint = value 34 | } 35 | 36 | var tokenInfoCallback: (TokenInfo) -> Map 37 | get() = callRouterConfiguration.tokenInfoCallback 38 | set(value) { 39 | callRouterConfiguration.tokenInfoCallback = value 40 | } 41 | 42 | var granters: List Granter> 43 | get() = callRouterConfiguration.granters 44 | set(value) { 45 | callRouterConfiguration.granters = value 46 | } 47 | 48 | var identityService: IdentityService? = null 49 | var clientService: ClientService? = null 50 | var tokenStore: TokenStore? = null 51 | var accessTokenConverter: AccessTokenConverter = UUIDAccessTokenConverter() 52 | var refreshTokenConverter: RefreshTokenConverter = UUIDRefreshTokenConverter() 53 | var codeTokenConverter: CodeTokenConverter = UUIDCodeTokenConverter() 54 | var accessTokenResponder: AccessTokenResponder = DefaultAccessTokenResponder 55 | } 56 | 57 | fun build( 58 | configurer: Configuration.() -> Unit, 59 | configuration: Configuration 60 | ): nl.myndocs.oauth2.config.Configuration { 61 | configurer(configuration) 62 | 63 | val grantingCallFactory: (CallContext) -> GrantingCall = { callContext -> 64 | object : GrantingCall { 65 | override val callContext = callContext 66 | override val identityService = configuration.identityService!! 67 | override val clientService = configuration.clientService!! 68 | override val tokenStore = configuration.tokenStore!! 69 | override val converters = Converters( 70 | configuration.accessTokenConverter, 71 | configuration.refreshTokenConverter, 72 | configuration.codeTokenConverter 73 | ) 74 | override val accessTokenResponder = configuration.accessTokenResponder 75 | } 76 | } 77 | return Configuration( 78 | CallRouterBuilder.build( 79 | configuration.callRouterConfiguration, 80 | grantingCallFactory 81 | ) 82 | ) 83 | } 84 | 85 | fun build(configurer: Configuration.() -> Unit): nl.myndocs.oauth2.config.Configuration { 86 | val configuration = Configuration() 87 | return build(configurer, configuration) 88 | } 89 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterRedirect.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.grant 2 | 3 | import nl.myndocs.oauth2.client.AuthorizedGrantType 4 | import nl.myndocs.oauth2.exception.InvalidClientException 5 | import nl.myndocs.oauth2.exception.InvalidGrantException 6 | import nl.myndocs.oauth2.exception.InvalidIdentityException 7 | import nl.myndocs.oauth2.exception.InvalidRequestException 8 | import nl.myndocs.oauth2.request.RedirectAuthorizationCodeRequest 9 | import nl.myndocs.oauth2.request.RedirectTokenRequest 10 | import nl.myndocs.oauth2.scope.ScopeParser 11 | import nl.myndocs.oauth2.token.AccessToken 12 | import nl.myndocs.oauth2.token.CodeToken 13 | 14 | fun GrantingCall.redirect(redirect: RedirectAuthorizationCodeRequest): CodeToken { 15 | checkMissingFields(redirect) 16 | 17 | val clientOf = clientService.clientOf(redirect.clientId!!) ?: throw InvalidClientException() 18 | if (!clientOf.redirectUris.contains(redirect.redirectUri)) { 19 | throw InvalidGrantException("invalid 'redirect_uri'") 20 | } 21 | 22 | with(AuthorizedGrantType.AUTHORIZATION_CODE) { 23 | if (!clientOf.authorizedGrantTypes.contains(this)) { 24 | throw InvalidGrantException("Authorize not allowed: '$this'") 25 | } 26 | } 27 | 28 | val identityOf = identityService.identityOf(clientOf, redirect.username!!) ?: throw InvalidIdentityException() 29 | 30 | val validIdentity = identityService.validCredentials(clientOf, identityOf, redirect.password!!) 31 | if (!validIdentity) { 32 | throw InvalidIdentityException() 33 | } 34 | 35 | var requestedScopes = ScopeParser.parseScopes(redirect.scope) 36 | if (redirect.scope == null) { 37 | requestedScopes = clientOf.clientScopes 38 | } 39 | 40 | validateScopes(clientOf, identityOf, requestedScopes) 41 | 42 | val codeToken = converters.codeTokenConverter.convertToToken( 43 | identityOf, 44 | clientOf.clientId, 45 | redirect.redirectUri!!, 46 | requestedScopes 47 | ) 48 | 49 | tokenStore.storeCodeToken(codeToken) 50 | 51 | return codeToken 52 | } 53 | 54 | fun GrantingCall.redirect(redirect: RedirectTokenRequest): AccessToken { 55 | checkMissingFields(redirect) 56 | 57 | val clientOf = clientService.clientOf(redirect.clientId!!) ?: throw InvalidClientException() 58 | if (!clientOf.redirectUris.contains(redirect.redirectUri)) { 59 | throw InvalidGrantException("invalid 'redirect_uri'") 60 | } 61 | 62 | with(AuthorizedGrantType.IMPLICIT) { 63 | if (!clientOf.authorizedGrantTypes.contains(this)) { 64 | throw InvalidGrantException("Authorize not allowed: '$this'") 65 | } 66 | } 67 | 68 | val identityOf = identityService.identityOf(clientOf, redirect.username!!) ?: throw InvalidIdentityException() 69 | 70 | val validIdentity = identityService.validCredentials(clientOf, identityOf, redirect.password!!) 71 | if (!validIdentity) { 72 | throw InvalidIdentityException() 73 | } 74 | 75 | var requestedScopes = ScopeParser.parseScopes(redirect.scope) 76 | if (redirect.scope == null) { 77 | // @TODO: This behavior is not in the spec and should be configurable https://tools.ietf.org/html/rfc6749#section-3.3 78 | requestedScopes = clientOf.clientScopes 79 | } 80 | 81 | validateScopes(clientOf, identityOf, requestedScopes) 82 | 83 | val accessToken = converters.accessTokenConverter.convertToToken( 84 | identityOf, 85 | clientOf.clientId, 86 | requestedScopes, 87 | null 88 | ) 89 | 90 | tokenStore.storeAccessToken(accessToken) 91 | 92 | return accessToken 93 | } 94 | 95 | private fun throwMissingField(field: String): Nothing = 96 | throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format(field)) 97 | 98 | private fun checkMissingFields(redirect: RedirectTokenRequest) = with(redirect) { 99 | when { 100 | clientId == null -> throwMissingField("client_id") 101 | username == null -> throwMissingField("username") 102 | password == null -> throwMissingField("password") 103 | redirectUri == null -> throwMissingField("redirect_uri") 104 | else -> this 105 | } 106 | } 107 | 108 | private fun checkMissingFields(redirect: RedirectAuthorizationCodeRequest) = with(redirect) { 109 | when { 110 | clientId == null -> throwMissingField("client_id") 111 | username == null -> throwMissingField("username") 112 | password == null -> throwMissingField("password") 113 | redirectUri == null -> throwMissingField("redirect_uri") 114 | else -> this 115 | } 116 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterDefault.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.grant 2 | 3 | import nl.myndocs.oauth2.client.Client 4 | import nl.myndocs.oauth2.exception.InvalidClientException 5 | import nl.myndocs.oauth2.exception.InvalidGrantException 6 | import nl.myndocs.oauth2.exception.InvalidRequestException 7 | import nl.myndocs.oauth2.exception.InvalidScopeException 8 | import nl.myndocs.oauth2.identity.Identity 9 | import nl.myndocs.oauth2.identity.TokenInfo 10 | import nl.myndocs.oauth2.request.* 11 | 12 | fun GrantingCall.grantPassword() = granter("password") { 13 | val accessToken = authorize( 14 | PasswordGrantRequest( 15 | callContext.formParameters["client_id"], 16 | callContext.formParameters["client_secret"], 17 | callContext.formParameters["username"], 18 | callContext.formParameters["password"], 19 | callContext.formParameters["scope"] 20 | ) 21 | ) 22 | 23 | callContext.respondHeader("Cache-Control", "no-store") 24 | callContext.respondHeader("Pragma", "no-cache") 25 | callContext.respondJson(accessTokenResponder.createResponse(accessToken)) 26 | } 27 | 28 | fun GrantingCall.grantClientCredentials() = granter("client_credentials") { 29 | val accessToken = authorize( 30 | ClientCredentialsRequest( 31 | callContext.formParameters["client_id"], 32 | callContext.formParameters["client_secret"], 33 | callContext.formParameters["scope"] 34 | ) 35 | ) 36 | 37 | callContext.respondHeader("Cache-Control", "no-store") 38 | callContext.respondHeader("Pragma", "no-cache") 39 | callContext.respondJson(accessTokenResponder.createResponse(accessToken)) 40 | } 41 | 42 | fun GrantingCall.grantRefreshToken() = granter("refresh_token") { 43 | val accessToken = refresh( 44 | RefreshTokenRequest( 45 | callContext.formParameters["client_id"], 46 | callContext.formParameters["client_secret"], 47 | callContext.formParameters["refresh_token"] 48 | ) 49 | ) 50 | 51 | callContext.respondHeader("Cache-Control", "no-store") 52 | callContext.respondHeader("Pragma", "no-cache") 53 | callContext.respondJson(accessTokenResponder.createResponse(accessToken)) 54 | } 55 | 56 | fun GrantingCall.grantAuthorizationCode() = granter("authorization_code") { 57 | val accessToken = authorize( 58 | AuthorizationCodeRequest( 59 | callContext.formParameters["client_id"], 60 | callContext.formParameters["client_secret"], 61 | callContext.formParameters["code"], 62 | callContext.formParameters["redirect_uri"] 63 | ) 64 | ) 65 | 66 | callContext.respondHeader("Cache-Control", "no-store") 67 | callContext.respondHeader("Pragma", "no-cache") 68 | callContext.respondJson(accessTokenResponder.createResponse(accessToken)) 69 | } 70 | 71 | internal val INVALID_REQUEST_FIELD_MESSAGE = "'%s' field is missing" 72 | 73 | fun GrantingCall.validateScopes( 74 | client: Client, 75 | identity: Identity, 76 | requestedScopes: Set 77 | ) { 78 | val scopesAllowed = scopesAllowed(client.clientScopes, requestedScopes) 79 | if (!scopesAllowed) { 80 | throw InvalidScopeException(requestedScopes.minus(client.clientScopes)) 81 | } 82 | 83 | val allowedScopes = identityService.allowedScopes(client, identity, requestedScopes) 84 | 85 | val ivalidScopes = requestedScopes.minus(allowedScopes) 86 | if (ivalidScopes.isNotEmpty()) { 87 | throw InvalidScopeException(ivalidScopes) 88 | } 89 | } 90 | 91 | fun GrantingCall.tokenInfo(accessToken: String): TokenInfo { 92 | val storedAccessToken = tokenStore.accessToken(accessToken) ?: throw InvalidGrantException() 93 | val client = clientService.clientOf(storedAccessToken.clientId) ?: throw InvalidClientException() 94 | val identity = storedAccessToken.identity?.let { identityService.identityOf(client, it.username) } 95 | 96 | return TokenInfo( 97 | identity, 98 | client, 99 | storedAccessToken.scopes 100 | ) 101 | } 102 | 103 | fun GrantingCall.throwExceptionIfUnverifiedClient(clientRequest: ClientRequest) { 104 | val clientId = clientRequest.clientId 105 | ?: throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("client_id")) 106 | 107 | val clientSecret = clientRequest.clientSecret 108 | ?: throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("client_secret")) 109 | 110 | val client = clientService.clientOf(clientId) ?: throw InvalidClientException() 111 | 112 | if (!clientService.validClient(client, clientSecret)) { 113 | throw InvalidClientException() 114 | } 115 | } 116 | 117 | fun GrantingCall.scopesAllowed(clientScopes: Set, requestedScopes: Set): Boolean { 118 | return clientScopes.containsAll(requestedScopes) 119 | } 120 | -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/grant/CallRouterAuthorize.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.grant 2 | 3 | import nl.myndocs.oauth2.client.AuthorizedGrantType 4 | import nl.myndocs.oauth2.exception.* 5 | import nl.myndocs.oauth2.request.AuthorizationCodeRequest 6 | import nl.myndocs.oauth2.request.ClientCredentialsRequest 7 | import nl.myndocs.oauth2.request.PasswordGrantRequest 8 | import nl.myndocs.oauth2.scope.ScopeParser 9 | import nl.myndocs.oauth2.token.AccessToken 10 | 11 | /** 12 | * @throws InvalidIdentityException 13 | * @throws InvalidClientException 14 | * @throws InvalidScopeException 15 | */ 16 | fun GrantingCall.authorize(passwordGrantRequest: PasswordGrantRequest): AccessToken { 17 | throwExceptionIfUnverifiedClient(passwordGrantRequest) 18 | 19 | if (passwordGrantRequest.username == null) { 20 | throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("username")) 21 | } 22 | 23 | if (passwordGrantRequest.password == null) { 24 | throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("password")) 25 | } 26 | 27 | val requestedClient = clientService.clientOf(passwordGrantRequest.clientId!!) ?: throw InvalidClientException() 28 | 29 | val authorizedGrantType = AuthorizedGrantType.PASSWORD 30 | if (!requestedClient.authorizedGrantTypes.contains(authorizedGrantType)) { 31 | throw InvalidGrantException("Authorize not allowed: '$authorizedGrantType'") 32 | } 33 | 34 | val requestedIdentity = identityService.identityOf( 35 | requestedClient, passwordGrantRequest.username 36 | ) 37 | 38 | if (requestedIdentity == null || !identityService.validCredentials( 39 | requestedClient, 40 | requestedIdentity, 41 | passwordGrantRequest.password 42 | ) 43 | ) { 44 | throw InvalidIdentityException() 45 | } 46 | 47 | var requestedScopes = ScopeParser.parseScopes(passwordGrantRequest.scope) 48 | .toSet() 49 | 50 | if (passwordGrantRequest.scope == null) { 51 | requestedScopes = requestedClient.clientScopes 52 | } 53 | 54 | validateScopes(requestedClient, requestedIdentity, requestedScopes) 55 | 56 | val accessToken = converters.accessTokenConverter.convertToToken( 57 | requestedIdentity, 58 | requestedClient.clientId, 59 | requestedScopes, 60 | converters.refreshTokenConverter.convertToToken( 61 | requestedIdentity, 62 | requestedClient.clientId, 63 | requestedScopes 64 | ) 65 | ) 66 | 67 | tokenStore.storeAccessToken(accessToken) 68 | 69 | return accessToken 70 | } 71 | 72 | fun GrantingCall.authorize(authorizationCodeRequest: AuthorizationCodeRequest): AccessToken { 73 | throwExceptionIfUnverifiedClient(authorizationCodeRequest) 74 | 75 | if (authorizationCodeRequest.code == null) { 76 | throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("code")) 77 | } 78 | 79 | if (authorizationCodeRequest.redirectUri == null) { 80 | throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("redirect_uri")) 81 | } 82 | 83 | val consumeCodeToken = tokenStore.consumeCodeToken(authorizationCodeRequest.code) 84 | ?: throw InvalidGrantException() 85 | 86 | 87 | if (consumeCodeToken.redirectUri != authorizationCodeRequest.redirectUri || consumeCodeToken.clientId != authorizationCodeRequest.clientId) { 88 | throw InvalidGrantException() 89 | } 90 | 91 | val accessToken = converters.accessTokenConverter.convertToToken( 92 | consumeCodeToken.identity, 93 | consumeCodeToken.clientId, 94 | consumeCodeToken.scopes, 95 | converters.refreshTokenConverter.convertToToken( 96 | consumeCodeToken.identity, 97 | consumeCodeToken.clientId, 98 | consumeCodeToken.scopes 99 | ) 100 | ) 101 | 102 | tokenStore.storeAccessToken(accessToken) 103 | 104 | return accessToken 105 | } 106 | 107 | fun GrantingCall.authorize(clientCredentialsRequest: ClientCredentialsRequest): AccessToken { 108 | throwExceptionIfUnverifiedClient(clientCredentialsRequest) 109 | 110 | val requestedClient = clientService.clientOf(clientCredentialsRequest.clientId!!) ?: throw InvalidClientException() 111 | 112 | val scopes = clientCredentialsRequest.scope 113 | ?.let { ScopeParser.parseScopes(it).toSet() } 114 | ?: requestedClient.clientScopes 115 | 116 | val accessToken = converters.accessTokenConverter.convertToToken( 117 | identity = null, 118 | clientId = clientCredentialsRequest.clientId, 119 | requestedScopes = scopes, 120 | refreshToken = converters.refreshTokenConverter.convertToToken( 121 | identity = null, 122 | clientId = clientCredentialsRequest.clientId, 123 | requestedScopes = scopes 124 | ) 125 | ) 126 | 127 | tokenStore.storeAccessToken(accessToken) 128 | 129 | return accessToken 130 | } 131 | -------------------------------------------------------------------------------- /oauth2-server-core/src/test/java/nl/myndocs/oauth2/ClientCredentialsTokenServiceTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2 2 | 3 | import io.mockk.every 4 | import io.mockk.impl.annotations.MockK 5 | import io.mockk.impl.annotations.RelaxedMockK 6 | import io.mockk.junit5.MockKExtension 7 | import io.mockk.verify 8 | import nl.myndocs.oauth2.client.AuthorizedGrantType 9 | import nl.myndocs.oauth2.client.Client 10 | import nl.myndocs.oauth2.client.ClientService 11 | import nl.myndocs.oauth2.exception.InvalidClientException 12 | import nl.myndocs.oauth2.grant.GrantingCall 13 | import nl.myndocs.oauth2.grant.authorize 14 | import nl.myndocs.oauth2.identity.IdentityService 15 | import nl.myndocs.oauth2.request.CallContext 16 | import nl.myndocs.oauth2.request.ClientCredentialsRequest 17 | import nl.myndocs.oauth2.response.AccessTokenResponder 18 | import nl.myndocs.oauth2.token.AccessToken 19 | import nl.myndocs.oauth2.token.RefreshToken 20 | import nl.myndocs.oauth2.token.TokenStore 21 | import nl.myndocs.oauth2.token.converter.AccessTokenConverter 22 | import nl.myndocs.oauth2.token.converter.CodeTokenConverter 23 | import nl.myndocs.oauth2.token.converter.Converters 24 | import nl.myndocs.oauth2.token.converter.RefreshTokenConverter 25 | import org.junit.jupiter.api.Assertions 26 | import org.junit.jupiter.api.BeforeEach 27 | import org.junit.jupiter.api.Test 28 | import org.junit.jupiter.api.extension.ExtendWith 29 | import java.time.Instant 30 | 31 | @ExtendWith(MockKExtension::class) 32 | internal class ClientCredentialsTokenServiceTest { 33 | @MockK 34 | lateinit var callContext: CallContext 35 | @MockK 36 | lateinit var identityService: IdentityService 37 | @MockK 38 | lateinit var clientService: ClientService 39 | @RelaxedMockK 40 | lateinit var tokenStore: TokenStore 41 | @MockK 42 | lateinit var accessTokenConverter: AccessTokenConverter 43 | @MockK 44 | lateinit var refreshTokenConverter: RefreshTokenConverter 45 | @MockK 46 | lateinit var codeTokenConverter: CodeTokenConverter 47 | @MockK 48 | lateinit var accessTokenResponder: AccessTokenResponder 49 | 50 | lateinit var grantingCall: GrantingCall 51 | 52 | @BeforeEach 53 | fun initialize() { 54 | grantingCall = object : GrantingCall { 55 | override val callContext = this@ClientCredentialsTokenServiceTest.callContext 56 | override val identityService = this@ClientCredentialsTokenServiceTest.identityService 57 | override val clientService = this@ClientCredentialsTokenServiceTest.clientService 58 | override val tokenStore = this@ClientCredentialsTokenServiceTest.tokenStore 59 | override val converters = Converters( 60 | this@ClientCredentialsTokenServiceTest.accessTokenConverter, 61 | this@ClientCredentialsTokenServiceTest.refreshTokenConverter, 62 | this@ClientCredentialsTokenServiceTest.codeTokenConverter 63 | ) 64 | override val accessTokenResponder = this@ClientCredentialsTokenServiceTest.accessTokenResponder 65 | } 66 | } 67 | private val clientId = "client-foo" 68 | private val clientSecret = "client-secret" 69 | private val scope = "scope1" 70 | private val scopes = setOf(scope) 71 | private val clientCredentialsRequest = ClientCredentialsRequest(clientId, clientSecret, scope) 72 | 73 | @Test 74 | fun validClientCredentialsGrant() { 75 | val client = Client(clientId, emptySet(), emptySet(), setOf(AuthorizedGrantType.CLIENT_CREDENTIALS)) 76 | val refreshToken = RefreshToken("test", Instant.now(), null, clientId, scopes) 77 | val accessToken = AccessToken("test", "bearer", Instant.now(), null, clientId, scopes, refreshToken) 78 | 79 | every { clientService.clientOf(clientId) } returns client 80 | every { clientService.validClient(client, clientSecret) } returns true 81 | every { refreshTokenConverter.convertToToken(null, clientId, scopes) } returns refreshToken 82 | every { accessTokenConverter.convertToToken(null, clientId, scopes, refreshToken) } returns accessToken 83 | 84 | grantingCall.authorize(clientCredentialsRequest) 85 | 86 | verify { tokenStore.storeAccessToken(accessToken) } 87 | } 88 | 89 | @Test 90 | fun nonExistingClientException() { 91 | every { clientService.clientOf(clientId) } returns null 92 | 93 | Assertions.assertThrows( 94 | InvalidClientException::class.java 95 | ) { grantingCall.authorize(clientCredentialsRequest) } 96 | } 97 | 98 | @Test 99 | fun invalidClientException() { 100 | val client = Client(clientId, emptySet(), emptySet(), setOf(AuthorizedGrantType.CLIENT_CREDENTIALS)) 101 | every { clientService.clientOf(clientId) } returns client 102 | every { clientService.validClient(client, clientSecret) } returns false 103 | 104 | Assertions.assertThrows( 105 | InvalidClientException::class.java 106 | ) { grantingCall.authorize(clientCredentialsRequest) } 107 | } 108 | 109 | @Test 110 | fun clientScopesAsFallback() { 111 | val clientCredentialsRequest = ClientCredentialsRequest( 112 | clientId, 113 | clientSecret, 114 | null 115 | ) 116 | 117 | val client = Client(clientId, setOf("scope1", "scope2"), setOf(), setOf(AuthorizedGrantType.CLIENT_CREDENTIALS)) 118 | val requestScopes = setOf("scope1", "scope2") 119 | val refreshToken = RefreshToken("test", Instant.now(), null, clientId, requestScopes) 120 | val accessToken = AccessToken("test", "bearer", Instant.now(), null, clientId, requestScopes, refreshToken) 121 | 122 | every { clientService.clientOf(clientId) } returns client 123 | every { clientService.validClient(client, clientSecret) } returns true 124 | every { refreshTokenConverter.convertToToken(null, clientId, requestScopes) } returns refreshToken 125 | every { accessTokenConverter.convertToToken(null, clientId, requestScopes, refreshToken) } returns accessToken 126 | 127 | grantingCall.authorize(clientCredentialsRequest) 128 | } 129 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin OAuth2 server 2 | ## Goal 3 | The goal of this project is to provide a simple OAuth2 library which can be implemented in any framework 4 | 5 | Configuring the oauth2 server for any framework should be simple and understandable. 6 | It encourages to adapt to existing implementations instead the other way around. 7 | 8 | # Frameworks 9 | ## Setup 10 | 11 | ### Maven 12 | 13 | ```xml 14 | 15 | 0.7.1 16 | 17 | 18 | 19 | 20 | nl.myndocs 21 | oauth2-server-core 22 | ${myndocs.oauth.version} 23 | 24 | 25 | 26 | 27 | nl.myndocs 28 | oauth2-server-client-inmemory 29 | ${myndocs.oauth.version} 30 | 31 | 32 | nl.myndocs 33 | oauth2-server-identity-inmemory 34 | ${myndocs.oauth.version} 35 | 36 | 37 | nl.myndocs 38 | oauth2-server-token-store-inmemory 39 | ${myndocs.oauth.version} 40 | 41 | 42 | ``` 43 | 44 | ### Gradle 45 | ```groovy 46 | dependencies { 47 | implementation "nl.myndocs:oauth2-server-core:$myndocs_oauth_version" 48 | // In memory dependencies 49 | implementation "nl.myndocs:oauth2-server-client-inmemory:$myndocs_oauth_version" 50 | implementation "nl.myndocs:oauth2-server-identity-inmemory:$myndocs_oauth_version" 51 | implementation "nl.myndocs:oauth2-server-token-store-inmemory:$myndocs_oauth_version" 52 | } 53 | ``` 54 | 55 | 56 | ### Framework implementation 57 | The following frameworks are supported: 58 | - [Ktor](docs/ktor.md) 59 | - [Javalin](docs/javalin.md) 60 | - [http4k](docs/http4k.md) 61 | - [Sparkjava](docs/sparkjava.md) 62 | 63 | ## Configuration 64 | ### Routing 65 | Default endpoints are configured: 66 | 67 | | Type | Relative url | 68 | | ----- | ------------- | 69 | | token | /oauth/token | 70 | | authorize | /oauth/authorize | 71 | | token info | /oauth/tokeninfo | 72 | 73 | These values can be overridden: 74 | ```kotlin 75 | tokenEndpoint = "/custom/token" 76 | authorizationEndpoint = "/custom/authorize" 77 | tokenInfoEndpoint = "/custom/tokeninfo" 78 | ``` 79 | 80 | ### In memory 81 | In memory implementations are provided to easily setup the project. 82 | 83 | #### Identity 84 | On the `InMemoryIdentity` identities can be registered. These are normally your users: 85 | ```kotlin 86 | identityService = InMemoryIdentity() 87 | .identity { 88 | username = "foo-1" 89 | password = "bar" 90 | } 91 | .identity { 92 | username = "foo-2" 93 | password = "bar" 94 | } 95 | ``` 96 | 97 | #### Client 98 | On the `InMemoryClient` clients can be registered: 99 | ```kotlin 100 | clientService = InMemoryClient() 101 | .client { 102 | clientId = "app1-client" 103 | clientSecret = "testpass" 104 | scopes = setOf("admin") 105 | redirectUris = setOf("https://localhost:8080/callback") 106 | authorizedGrantTypes = setOf( 107 | AuthorizedGrantType.AUTHORIZATION_CODE, 108 | AuthorizedGrantType.PASSWORD, 109 | AuthorizedGrantType.IMPLICIT, 110 | AuthorizedGrantType.REFRESH_TOKEN 111 | ) 112 | } 113 | .client { 114 | clientId = "app2-client" 115 | clientSecret = "testpass" 116 | scopes = setOf("user") 117 | redirectUris = setOf("https://localhost:8080/callback") 118 | authorizedGrantTypes = setOf( 119 | AuthorizedGrantType.AUTHORIZATION_CODE 120 | ) 121 | } 122 | ``` 123 | 124 | #### Token store 125 | The `InMemoryTokenStore` stores all kinds of tokens. 126 | ```kotlin 127 | tokenStore = InMemoryTokenStore() 128 | ``` 129 | 130 | ### Converters 131 | 132 | #### Access token converter 133 | By default `UUIDAccessTokenConverter` is used. With a default time-out of 1 hour. To override the time-out for example to half an hour: 134 | ```kotlin 135 | accessTokenConverter = UUIDAccessTokenConverter(1800) 136 | ``` 137 | 138 | To use JWT include the following dependency: 139 | ```xml 140 | 141 | nl.myndocs 142 | oauth2-server-jwt 143 | ${myndocs.oauth.version} 144 | 145 | ``` 146 | This uses [auth0 jwt](https://github.com/auth0/java-jwt). To configure: 147 | ```kotlin 148 | accessTokenConverter = JwtAccessTokenConverter( 149 | algorithm = Algorithm.HMAC256("test123"), // mandatory 150 | accessTokenExpireInSeconds = 1800, // optional default 3600 151 | jwtBuilder = DefaultJwtBuilder // optional uses DefaultJwtBuilder by default 152 | ) 153 | ``` 154 | 155 | #### Refresh token converter 156 | By default `UUIDRefreshTokenConverter` is used. With a default time-out of 1 hour. To override the time-out for example to half an hour: 157 | ```kotlin 158 | refreshTokenConverter = UUIDRefreshTokenConverter(1800) 159 | ``` 160 | 161 | To use JWT include the following dependency: 162 | ```xml 163 | 164 | nl.myndocs 165 | oauth2-server-jwt 166 | ${myndocs.oauth.version} 167 | 168 | ``` 169 | This uses [auth0 jwt](https://github.com/auth0/java-jwt). To configure: 170 | ```kotlin 171 | refreshTokenConverter = JwtRefreshTokenConverter( 172 | algorithm = Algorithm.HMAC256("test123"), // mandatory 173 | refreshTokenExpireInSeconds = 1800, // optional default 86400 174 | jwtBuilder = DefaultJwtBuilder // optional uses DefaultJwtBuilder by default 175 | ) 176 | ``` 177 | #### Code token converter 178 | By default `UUIDCodeTokenConverter` is used. With a default time-out of 5 minutes. To override the time-out for example 2 minutes: 179 | ```kotlin 180 | codeTokenConverter = UUIDCodeTokenConverter(120) 181 | ``` 182 | -------------------------------------------------------------------------------- /oauth2-server-integration-base/src/main/java/nl/myndocs/oauth2/integration/BaseIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2.integration 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.module.kotlin.registerKotlinModule 5 | import nl.myndocs.oauth2.client.AuthorizedGrantType 6 | import nl.myndocs.oauth2.client.inmemory.InMemoryClient 7 | import nl.myndocs.oauth2.config.ConfigurationBuilder 8 | import nl.myndocs.oauth2.identity.inmemory.InMemoryIdentity 9 | import nl.myndocs.oauth2.tokenstore.inmemory.InMemoryTokenStore 10 | import okhttp3.* 11 | import org.hamcrest.CoreMatchers.* 12 | import org.hamcrest.MatcherAssert.assertThat 13 | import org.junit.jupiter.api.Test 14 | import java.util.* 15 | 16 | abstract class BaseIntegrationTest { 17 | var localPort: Int? = null 18 | val configBuilder: ConfigurationBuilder.Configuration.() -> Unit = { 19 | identityService = InMemoryIdentity() 20 | .identity { 21 | username = "foo" 22 | password = "bar" 23 | } 24 | clientService = InMemoryClient() 25 | .client { 26 | clientId = "testapp" 27 | clientSecret = "testpass" 28 | scopes = setOf("trusted") 29 | redirectUris = setOf("http://localhost:8080/callback") 30 | authorizedGrantTypes = setOf( 31 | AuthorizedGrantType.AUTHORIZATION_CODE, 32 | AuthorizedGrantType.PASSWORD, 33 | AuthorizedGrantType.IMPLICIT, 34 | AuthorizedGrantType.REFRESH_TOKEN 35 | ) 36 | } 37 | tokenStore = InMemoryTokenStore() 38 | 39 | } 40 | 41 | private val objectMapper = ObjectMapper().registerKotlinModule() 42 | 43 | @Test 44 | fun `test password grant flow`() { 45 | val client = OkHttpClient() 46 | val body = FormBody.Builder() 47 | .add("grant_type", "password") 48 | .add("username", "foo") 49 | .add("password", "bar") 50 | .add("client_id", "testapp") 51 | .add("client_secret", "testpass") 52 | .build() 53 | 54 | val url = buildOauthTokenUri() 55 | 56 | val request = Request.Builder() 57 | .url(url) 58 | .post(body) 59 | .build() 60 | 61 | val response = client.newCall(request) 62 | .execute() 63 | 64 | val values = objectMapper.readMap(response.body()!!.string()) 65 | 66 | assertThat(values["access_token"], `is`(notNullValue())) 67 | assertThat(UUID.fromString(values["access_token"] as String), `is`(instanceOf(UUID::class.java))) 68 | 69 | response.close() 70 | } 71 | 72 | @Test 73 | fun `test authorization grant flow`() { 74 | 75 | val client = OkHttpClient.Builder() 76 | .followRedirects(false) 77 | .build() 78 | 79 | val url = HttpUrl.Builder() 80 | .scheme("http") 81 | .host("localhost") 82 | .port(localPort!!) 83 | .addPathSegment("oauth") 84 | .addPathSegment("authorize") 85 | .setQueryParameter("response_type", "code") 86 | .setQueryParameter("client_id", "testapp") 87 | .setQueryParameter("redirect_uri", "http://localhost:8080/callback") 88 | .build() 89 | 90 | val request = Request.Builder() 91 | .addHeader("Authorization", Credentials.basic("foo", "bar")) 92 | .url(url) 93 | .get() 94 | .build() 95 | 96 | val response = client.newCall(request) 97 | .execute() 98 | 99 | response.close() 100 | 101 | val body = FormBody.Builder() 102 | .add("grant_type", "authorization_code") 103 | .add("code", response.header("location")!!.asQueryParameters()["code"]) 104 | .add("redirect_uri", "http://localhost:8080/callback") 105 | .add("client_id", "testapp") 106 | .add("client_secret", "testpass") 107 | .build() 108 | 109 | val tokenUrl = buildOauthTokenUri() 110 | 111 | val tokenRequest = Request.Builder() 112 | .url(tokenUrl) 113 | .post(body) 114 | .build() 115 | 116 | val tokenResponse = client.newCall(tokenRequest) 117 | .execute() 118 | 119 | val values = objectMapper.readMap(tokenResponse.body()!!.string()) 120 | assertThat(values["access_token"], `is`(notNullValue())) 121 | assertThat(UUID.fromString(values["access_token"] as String), `is`(instanceOf(UUID::class.java))) 122 | 123 | tokenResponse.close() 124 | } 125 | 126 | @Test 127 | fun `test client credentials flow`() { 128 | val client = OkHttpClient() 129 | val body = FormBody.Builder() 130 | .add("grant_type", "client_credentials") 131 | .add("client_id", "testapp") 132 | .add("client_secret", "testpass") 133 | .build() 134 | 135 | val tokenRequest = Request.Builder() 136 | .url(buildOauthTokenUri()) 137 | .post(body) 138 | .build() 139 | 140 | val tokenResponse = client.newCall(tokenRequest) 141 | .execute() 142 | 143 | val values = objectMapper.readMap(tokenResponse.body()!!.string()) 144 | assertThat(values["access_token"], `is`(notNullValue())) 145 | assertThat(UUID.fromString(values["access_token"] as String), `is`(instanceOf(UUID::class.java))) 146 | 147 | tokenResponse.close() 148 | 149 | } 150 | 151 | private fun buildOauthTokenUri() = 152 | HttpUrl.Builder() 153 | .scheme("http") 154 | .host("localhost") 155 | .port(localPort!!) 156 | .addPathSegment("oauth") 157 | .addPathSegment("token") 158 | .build() 159 | } 160 | 161 | fun ObjectMapper.readMap(content: String) = this.readValue(content, Map::class.java) 162 | 163 | fun String.asQueryParameters() = 164 | split("?")[1] 165 | .split("&") 166 | .map { it.split("=") } 167 | .associate { Pair(it[0], it[1]) } 168 | -------------------------------------------------------------------------------- /oauth2-server-core/src/test/java/nl/myndocs/oauth2/RefreshTokenGrantTokenServiceTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2 2 | 3 | import io.mockk.every 4 | import io.mockk.impl.annotations.MockK 5 | import io.mockk.impl.annotations.RelaxedMockK 6 | import io.mockk.junit5.MockKExtension 7 | import io.mockk.verify 8 | import nl.myndocs.oauth2.client.AuthorizedGrantType 9 | import nl.myndocs.oauth2.client.Client 10 | import nl.myndocs.oauth2.client.ClientService 11 | import nl.myndocs.oauth2.exception.InvalidClientException 12 | import nl.myndocs.oauth2.exception.InvalidGrantException 13 | import nl.myndocs.oauth2.exception.InvalidRequestException 14 | import nl.myndocs.oauth2.grant.GrantingCall 15 | import nl.myndocs.oauth2.grant.refresh 16 | import nl.myndocs.oauth2.identity.Identity 17 | import nl.myndocs.oauth2.identity.IdentityService 18 | import nl.myndocs.oauth2.request.CallContext 19 | import nl.myndocs.oauth2.request.RefreshTokenRequest 20 | import nl.myndocs.oauth2.response.AccessTokenResponder 21 | import nl.myndocs.oauth2.token.AccessToken 22 | import nl.myndocs.oauth2.token.RefreshToken 23 | import nl.myndocs.oauth2.token.TokenStore 24 | import nl.myndocs.oauth2.token.converter.AccessTokenConverter 25 | import nl.myndocs.oauth2.token.converter.CodeTokenConverter 26 | import nl.myndocs.oauth2.token.converter.Converters 27 | import nl.myndocs.oauth2.token.converter.RefreshTokenConverter 28 | import org.junit.jupiter.api.Assertions 29 | import org.junit.jupiter.api.BeforeEach 30 | import org.junit.jupiter.api.Test 31 | import org.junit.jupiter.api.extension.ExtendWith 32 | import java.time.Instant 33 | 34 | @ExtendWith(MockKExtension::class) 35 | internal class RefreshTokenGrantTokenServiceTest { 36 | @MockK 37 | lateinit var callContext: CallContext 38 | @MockK 39 | lateinit var identityService: IdentityService 40 | @MockK 41 | lateinit var clientService: ClientService 42 | @RelaxedMockK 43 | lateinit var tokenStore: TokenStore 44 | @MockK 45 | lateinit var accessTokenConverter: AccessTokenConverter 46 | @MockK 47 | lateinit var refreshTokenConverter: RefreshTokenConverter 48 | @MockK 49 | lateinit var codeTokenConverter: CodeTokenConverter 50 | @MockK 51 | lateinit var accessTokenResponder: AccessTokenResponder 52 | 53 | lateinit var grantingCall: GrantingCall 54 | 55 | @BeforeEach 56 | fun initialize() { 57 | grantingCall = object : GrantingCall { 58 | override val callContext = this@RefreshTokenGrantTokenServiceTest.callContext 59 | override val identityService = this@RefreshTokenGrantTokenServiceTest.identityService 60 | override val clientService = this@RefreshTokenGrantTokenServiceTest.clientService 61 | override val tokenStore = this@RefreshTokenGrantTokenServiceTest.tokenStore 62 | override val converters = Converters( 63 | this@RefreshTokenGrantTokenServiceTest.accessTokenConverter, 64 | this@RefreshTokenGrantTokenServiceTest.refreshTokenConverter, 65 | this@RefreshTokenGrantTokenServiceTest.codeTokenConverter 66 | ) 67 | override val accessTokenResponder = this@RefreshTokenGrantTokenServiceTest.accessTokenResponder 68 | } 69 | } 70 | 71 | val clientId = "client-foo" 72 | val clientSecret = "client-bar" 73 | val refreshToken = "refresh-token" 74 | val username = "foo-user" 75 | val scope = "scope1" 76 | val scopes = setOf(scope) 77 | val identity = Identity(username) 78 | 79 | val refreshTokenRequest = RefreshTokenRequest( 80 | clientId, 81 | clientSecret, 82 | refreshToken 83 | ) 84 | 85 | @Test 86 | fun validRefreshToken() { 87 | val client = Client(clientId, setOf("scope1", "scope2"), setOf(), setOf(AuthorizedGrantType.REFRESH_TOKEN)) 88 | val token = RefreshToken("test", Instant.now(), identity, clientId, scopes) 89 | val newRefreshToken = RefreshToken("new-test", Instant.now(), identity, clientId, scopes) 90 | val accessToken = AccessToken("test", "bearer", Instant.now(), identity, clientId, scopes, newRefreshToken) 91 | val identity = Identity(username) 92 | 93 | every { clientService.clientOf(clientId) } returns client 94 | every { clientService.validClient(client, clientSecret) } returns true 95 | every { tokenStore.refreshToken(refreshToken) } returns token 96 | every { identityService.identityOf(client, username) } returns identity 97 | every { refreshTokenConverter.convertToToken(token) } returns newRefreshToken 98 | every { accessTokenConverter.convertToToken(identity, clientId, scopes, newRefreshToken) } returns accessToken 99 | 100 | grantingCall.refresh(refreshTokenRequest) 101 | 102 | 103 | verify { tokenStore.storeAccessToken(accessToken) } 104 | } 105 | 106 | @Test 107 | fun missingRefreshToken() { 108 | val client = Client(clientId, setOf("scope1", "scope2"), setOf(), setOf(AuthorizedGrantType.REFRESH_TOKEN)) 109 | 110 | every { clientService.clientOf(clientId) } returns client 111 | every { clientService.validClient(client, clientSecret) } returns true 112 | 113 | val refreshTokenRequest = RefreshTokenRequest( 114 | clientId, 115 | clientSecret, 116 | null 117 | ) 118 | 119 | Assertions.assertThrows( 120 | InvalidRequestException::class.java 121 | ) { grantingCall.refresh(refreshTokenRequest) } 122 | } 123 | 124 | @Test 125 | fun nonExistingClientException() { 126 | every { clientService.clientOf(clientId) } returns null 127 | 128 | Assertions.assertThrows( 129 | InvalidClientException::class.java 130 | ) { grantingCall.refresh(refreshTokenRequest) } 131 | } 132 | 133 | @Test 134 | fun invalidClientException() { 135 | val client = Client(clientId, setOf(), setOf(), setOf(AuthorizedGrantType.REFRESH_TOKEN)) 136 | every { clientService.clientOf(clientId) } returns client 137 | every { clientService.validClient(client, clientSecret) } returns false 138 | 139 | Assertions.assertThrows( 140 | InvalidClientException::class.java 141 | ) { grantingCall.refresh(refreshTokenRequest) } 142 | } 143 | 144 | @Test 145 | fun storedClientDoesNotMatchRequestedException() { 146 | val client = Client(clientId, setOf("scope1", "scope2"), setOf(), setOf(AuthorizedGrantType.REFRESH_TOKEN)) 147 | val token = RefreshToken("test", Instant.now(), identity, "wrong-client", scopes) 148 | 149 | every { clientService.clientOf(clientId) } returns client 150 | every { clientService.validClient(client, clientSecret) } returns true 151 | every { tokenStore.refreshToken(refreshToken) } returns token 152 | 153 | Assertions.assertThrows( 154 | InvalidGrantException::class.java 155 | ) { grantingCall.refresh(refreshTokenRequest) } 156 | } 157 | } -------------------------------------------------------------------------------- /oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2 2 | 3 | import nl.myndocs.oauth2.authenticator.Credentials 4 | import nl.myndocs.oauth2.exception.* 5 | import nl.myndocs.oauth2.grant.Granter 6 | import nl.myndocs.oauth2.grant.GrantingCall 7 | import nl.myndocs.oauth2.grant.redirect 8 | import nl.myndocs.oauth2.grant.tokenInfo 9 | import nl.myndocs.oauth2.identity.TokenInfo 10 | import nl.myndocs.oauth2.request.CallContext 11 | import nl.myndocs.oauth2.request.RedirectAuthorizationCodeRequest 12 | import nl.myndocs.oauth2.request.RedirectTokenRequest 13 | import nl.myndocs.oauth2.request.headerCaseInsensitive 14 | import nl.myndocs.oauth2.router.RedirectRouter 15 | import nl.myndocs.oauth2.router.RedirectRouterResponse 16 | 17 | class CallRouter( 18 | val tokenEndpoint: String, 19 | val authorizeEndpoint: String, 20 | val tokenInfoEndpoint: String, 21 | private val tokenInfoCallback: (TokenInfo) -> Map, 22 | private val granters: List Granter>, 23 | private val grantingCallFactory: (CallContext) -> GrantingCall 24 | ) : RedirectRouter { 25 | companion object { 26 | const val METHOD_POST = "post" 27 | const val METHOD_GET = "get" 28 | 29 | const val STATUS_BAD_REQUEST = 400 30 | const val STATUS_UNAUTHORIZED = 401 31 | 32 | private val unauthorizedResponse = mapOf("message" to "unauthorized") 33 | } 34 | 35 | fun route(callContext: CallContext) { 36 | when (callContext.path) { 37 | tokenEndpoint -> routeTokenEndpoint(callContext) 38 | tokenInfoEndpoint -> routeTokenInfoEndpoint(callContext) 39 | } 40 | } 41 | 42 | override fun route(callContext: CallContext, credentials: Credentials?): RedirectRouterResponse { 43 | return when (callContext.path) { 44 | authorizeEndpoint -> routeAuthorizeEndpoint(callContext, credentials) 45 | else -> throw NoRoutesFoundException("Route '${callContext.path}' not found") 46 | } 47 | } 48 | 49 | private fun routeTokenEndpoint(callContext: CallContext) { 50 | if (callContext.method.toLowerCase() != METHOD_POST) { 51 | return 52 | } 53 | 54 | try { 55 | val grantType = callContext.formParameters["grant_type"] 56 | ?: throw InvalidRequestException("'grant_type' not given") 57 | 58 | val grantingCall = grantingCallFactory(callContext) 59 | 60 | val granterMap = granters.associate { 61 | val granter = grantingCall.it() 62 | granter.grantType to granter 63 | } 64 | 65 | val allowedGrantTypes = granterMap.keys 66 | 67 | if (!allowedGrantTypes.contains(grantType)) { 68 | throw InvalidGrantException("'grant_type' with value '$grantType' not allowed") 69 | } 70 | 71 | granterMap[grantType]!!.callback.invoke() 72 | } catch (oauthException: OauthException) { 73 | callContext.respondStatus(STATUS_BAD_REQUEST) 74 | callContext.respondJson(oauthException.toMap()) 75 | } 76 | } 77 | 78 | fun routeAuthorizationCodeRedirect( 79 | callContext: CallContext, 80 | credentials: Credentials? 81 | ): RedirectRouterResponse { 82 | val queryParameters = callContext.queryParameters 83 | try { 84 | val redirect = grantingCallFactory(callContext).redirect( 85 | RedirectAuthorizationCodeRequest( 86 | queryParameters["client_id"], 87 | queryParameters["redirect_uri"], 88 | credentials?.username, 89 | credentials?.password, 90 | queryParameters["scope"] 91 | ) 92 | ) 93 | 94 | var stateQueryParameter = "" 95 | 96 | if (queryParameters["state"] != null) { 97 | stateQueryParameter = "&state=" + queryParameters["state"] 98 | } 99 | 100 | callContext.redirect(queryParameters["redirect_uri"] + "?code=${redirect.codeToken}$stateQueryParameter") 101 | 102 | return RedirectRouterResponse(true) 103 | } catch (unverifiedIdentityException: InvalidIdentityException) { 104 | callContext.respondUnauthorized() 105 | 106 | return RedirectRouterResponse(false) 107 | } 108 | } 109 | 110 | fun routeAccessTokenRedirect( 111 | callContext: CallContext, 112 | credentials: Credentials? 113 | ): RedirectRouterResponse { 114 | val queryParameters = callContext.queryParameters 115 | 116 | try { 117 | val redirect = grantingCallFactory(callContext).redirect( 118 | RedirectTokenRequest( 119 | queryParameters["client_id"], 120 | queryParameters["redirect_uri"], 121 | credentials?.username, 122 | credentials?.password, 123 | queryParameters["scope"] 124 | ) 125 | ) 126 | 127 | var stateQueryParameter = "" 128 | if (queryParameters["state"] != null) { 129 | stateQueryParameter = "&state=" + queryParameters["state"] 130 | } 131 | 132 | callContext.redirect( 133 | queryParameters["redirect_uri"] + "#access_token=${redirect.accessToken}" + 134 | "&token_type=bearer&expires_in=${redirect.expiresIn()}$stateQueryParameter" 135 | ) 136 | 137 | return RedirectRouterResponse(true) 138 | } catch (unverifiedIdentityException: InvalidIdentityException) { 139 | callContext.respondUnauthorized() 140 | 141 | return RedirectRouterResponse(false) 142 | } 143 | } 144 | 145 | private fun routeAuthorizeEndpoint(callContext: CallContext, credentials: Credentials?): RedirectRouterResponse { 146 | try { 147 | if (!arrayOf(METHOD_GET, METHOD_POST).contains(callContext.method.toLowerCase())) { 148 | return RedirectRouterResponse(false) 149 | } 150 | 151 | val responseType = callContext.queryParameters["response_type"] 152 | ?: throw InvalidRequestException("'response_type' not given") 153 | 154 | return when (responseType) { 155 | "code" -> routeAuthorizationCodeRedirect(callContext, credentials) 156 | "token" -> routeAccessTokenRedirect(callContext, credentials) 157 | else -> throw InvalidGrantException("'grant_type' with value '$responseType' not allowed") 158 | } 159 | } catch (invalidIdentityException: InvalidIdentityException) { 160 | callContext.respondStatus(STATUS_UNAUTHORIZED) 161 | callContext.respondJson(invalidIdentityException.toMap()) 162 | return RedirectRouterResponse(false) 163 | } catch (oauthException: OauthException) { 164 | callContext.respondStatus(STATUS_BAD_REQUEST) 165 | callContext.respondJson(oauthException.toMap()) 166 | 167 | return RedirectRouterResponse(false) 168 | } 169 | } 170 | 171 | private fun routeTokenInfoEndpoint(callContext: CallContext) { 172 | if (callContext.method.toLowerCase() != METHOD_GET) { 173 | return 174 | } 175 | 176 | val authorization = callContext.headerCaseInsensitive("Authorization") 177 | 178 | if (authorization == null || !authorization.startsWith("bearer ", true)) { 179 | callContext.respondUnauthorized() 180 | return 181 | } 182 | 183 | val token = authorization.substring(7) 184 | val tokenInfoCallback = tokenInfoCallback(grantingCallFactory(callContext).tokenInfo(token)) 185 | 186 | callContext.respondJson(tokenInfoCallback) 187 | } 188 | 189 | private fun CallContext.respondUnauthorized() { 190 | this.respondStatus(STATUS_UNAUTHORIZED) 191 | this.respondJson(unauthorizedResponse) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /oauth2-server-core/src/test/java/nl/myndocs/oauth2/AuthorizationCodeGrantTokenServiceTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2 2 | 3 | import io.mockk.every 4 | import io.mockk.impl.annotations.MockK 5 | import io.mockk.impl.annotations.RelaxedMockK 6 | import io.mockk.junit5.MockKExtension 7 | import nl.myndocs.oauth2.client.AuthorizedGrantType 8 | import nl.myndocs.oauth2.client.Client 9 | import nl.myndocs.oauth2.client.ClientService 10 | import nl.myndocs.oauth2.exception.InvalidClientException 11 | import nl.myndocs.oauth2.exception.InvalidGrantException 12 | import nl.myndocs.oauth2.exception.InvalidRequestException 13 | import nl.myndocs.oauth2.grant.GrantingCall 14 | import nl.myndocs.oauth2.grant.authorize 15 | import nl.myndocs.oauth2.identity.Identity 16 | import nl.myndocs.oauth2.identity.IdentityService 17 | import nl.myndocs.oauth2.request.AuthorizationCodeRequest 18 | import nl.myndocs.oauth2.request.CallContext 19 | import nl.myndocs.oauth2.response.AccessTokenResponder 20 | import nl.myndocs.oauth2.token.AccessToken 21 | import nl.myndocs.oauth2.token.CodeToken 22 | import nl.myndocs.oauth2.token.RefreshToken 23 | import nl.myndocs.oauth2.token.TokenStore 24 | import nl.myndocs.oauth2.token.converter.AccessTokenConverter 25 | import nl.myndocs.oauth2.token.converter.CodeTokenConverter 26 | import nl.myndocs.oauth2.token.converter.Converters 27 | import nl.myndocs.oauth2.token.converter.RefreshTokenConverter 28 | import org.junit.jupiter.api.Assertions.assertThrows 29 | import org.junit.jupiter.api.BeforeEach 30 | import org.junit.jupiter.api.Test 31 | import org.junit.jupiter.api.extension.ExtendWith 32 | import java.time.Instant 33 | 34 | @ExtendWith(MockKExtension::class) 35 | internal class AuthorizationCodeGrantTokenServiceTest { 36 | @MockK 37 | lateinit var callContext: CallContext 38 | @MockK 39 | lateinit var identityService: IdentityService 40 | @MockK 41 | lateinit var clientService: ClientService 42 | @RelaxedMockK 43 | lateinit var tokenStore: TokenStore 44 | @MockK 45 | lateinit var accessTokenConverter: AccessTokenConverter 46 | @MockK 47 | lateinit var refreshTokenConverter: RefreshTokenConverter 48 | @MockK 49 | lateinit var codeTokenConverter: CodeTokenConverter 50 | @MockK 51 | lateinit var accessTokenResponder: AccessTokenResponder 52 | 53 | lateinit var grantingCall: GrantingCall 54 | 55 | @BeforeEach 56 | fun initialize() { 57 | grantingCall = object : GrantingCall { 58 | override val callContext = this@AuthorizationCodeGrantTokenServiceTest.callContext 59 | override val identityService = this@AuthorizationCodeGrantTokenServiceTest.identityService 60 | override val clientService = this@AuthorizationCodeGrantTokenServiceTest.clientService 61 | override val tokenStore = this@AuthorizationCodeGrantTokenServiceTest.tokenStore 62 | override val converters = Converters( 63 | this@AuthorizationCodeGrantTokenServiceTest.accessTokenConverter, 64 | this@AuthorizationCodeGrantTokenServiceTest.refreshTokenConverter, 65 | this@AuthorizationCodeGrantTokenServiceTest.codeTokenConverter 66 | ) 67 | override val accessTokenResponder = this@AuthorizationCodeGrantTokenServiceTest.accessTokenResponder 68 | } 69 | } 70 | 71 | val clientId = "client-foo" 72 | val clientSecret = "client-bar" 73 | val code = "user-foo" 74 | val redirectUri = "http://foo.lcoalhost" 75 | val username = "user-foo" 76 | val identity = Identity(username) 77 | 78 | val authorizationCodeRequest = AuthorizationCodeRequest( 79 | clientId, 80 | clientSecret, 81 | code, 82 | redirectUri 83 | ) 84 | 85 | @Test 86 | fun validAuthorizationCodeGrant() { 87 | val requestScopes = setOf("scope1") 88 | 89 | val client = Client(clientId, setOf("scope1", "scope2"), setOf(), setOf(AuthorizedGrantType.AUTHORIZATION_CODE)) 90 | val identity = Identity(username) 91 | val codeToken = CodeToken(code, Instant.now(), identity, clientId, redirectUri, requestScopes) 92 | 93 | val refreshToken = RefreshToken("test", Instant.now(), identity, clientId, requestScopes) 94 | val accessToken = AccessToken("test", "bearer", Instant.now(), identity, clientId, requestScopes, refreshToken) 95 | 96 | every { clientService.clientOf(clientId) } returns client 97 | every { clientService.validClient(client, clientSecret) } returns true 98 | every { identityService.identityOf(client, username) } returns identity 99 | every { tokenStore.consumeCodeToken(code) } returns codeToken 100 | every { refreshTokenConverter.convertToToken(identity, clientId, requestScopes) } returns refreshToken 101 | every { accessTokenConverter.convertToToken(identity, clientId, requestScopes, refreshToken) } returns accessToken 102 | 103 | grantingCall.authorize(authorizationCodeRequest) 104 | } 105 | 106 | @Test 107 | fun nonExistingClientException() { 108 | every { clientService.clientOf(clientId) } returns null 109 | 110 | assertThrows( 111 | InvalidClientException::class.java 112 | ) { grantingCall.authorize(authorizationCodeRequest) } 113 | } 114 | 115 | @Test 116 | fun invalidClientException() { 117 | val client = Client(clientId, setOf(), setOf(), setOf(AuthorizedGrantType.AUTHORIZATION_CODE)) 118 | every { clientService.clientOf(clientId) } returns client 119 | every { clientService.validClient(client, clientSecret) } returns false 120 | 121 | assertThrows( 122 | InvalidClientException::class.java 123 | ) { grantingCall.authorize(authorizationCodeRequest) } 124 | } 125 | 126 | @Test 127 | fun missingCodeException() { 128 | val authorizationCodeRequest = AuthorizationCodeRequest( 129 | clientId, 130 | clientSecret, 131 | null, 132 | redirectUri 133 | ) 134 | 135 | val client = Client(clientId, setOf(), setOf(), setOf(AuthorizedGrantType.AUTHORIZATION_CODE)) 136 | every { clientService.clientOf(clientId) } returns client 137 | every { clientService.validClient(client, clientSecret) } returns true 138 | 139 | assertThrows( 140 | InvalidRequestException::class.java 141 | ) { grantingCall.authorize(authorizationCodeRequest) } 142 | } 143 | 144 | @Test 145 | fun missingRedirectUriException() { 146 | val authorizationCodeRequest = AuthorizationCodeRequest( 147 | clientId, 148 | clientSecret, 149 | code, 150 | null 151 | ) 152 | 153 | val client = Client(clientId, setOf(), setOf(), setOf(AuthorizedGrantType.AUTHORIZATION_CODE)) 154 | every { clientService.clientOf(clientId) } returns client 155 | every { clientService.validClient(client, clientSecret) } returns true 156 | 157 | assertThrows( 158 | InvalidRequestException::class.java 159 | ) { grantingCall.authorize(authorizationCodeRequest) } 160 | } 161 | 162 | @Test 163 | fun invalidRedirectUriException() { 164 | val wrongRedirectUri = "" 165 | val requestScopes = setOf("scope1") 166 | 167 | val client = Client(clientId, setOf("scope1", "scope2"), setOf(), setOf(AuthorizedGrantType.AUTHORIZATION_CODE)) 168 | val codeToken = CodeToken(code, Instant.now(), identity, clientId, wrongRedirectUri, requestScopes) 169 | 170 | val refreshToken = RefreshToken("test", Instant.now(), identity, clientId, requestScopes) 171 | val accessToken = AccessToken("test", "bearer", Instant.now(), identity, clientId, requestScopes, refreshToken) 172 | 173 | every { clientService.clientOf(clientId) } returns client 174 | every { clientService.validClient(client, clientSecret) } returns true 175 | every { tokenStore.consumeCodeToken(code) } returns codeToken 176 | every { refreshTokenConverter.convertToToken(identity, clientId, requestScopes) } returns refreshToken 177 | every { accessTokenConverter.convertToToken(identity, clientId, requestScopes, refreshToken) } returns accessToken 178 | 179 | assertThrows( 180 | InvalidGrantException::class.java 181 | ) { grantingCall.authorize(authorizationCodeRequest) } 182 | } 183 | 184 | @Test 185 | fun invalidCodeException() { 186 | val client = Client(clientId, setOf("scope1", "scope2"), setOf(), setOf(AuthorizedGrantType.AUTHORIZATION_CODE)) 187 | 188 | every { clientService.clientOf(clientId) } returns client 189 | every { clientService.validClient(client, clientSecret) } returns true 190 | every { tokenStore.consumeCodeToken(code) } returns null 191 | 192 | assertThrows( 193 | InvalidGrantException::class.java 194 | ) { grantingCall.authorize(authorizationCodeRequest) } 195 | } 196 | 197 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | nl.myndocs 8 | kotlin-oauth2-server 9 | pom 10 | 0.7.1 11 | Kotlin OAuth2 server 12 | Flexible OAuth2 implementation 13 | https://github.com/myndocs/kotlin-oauth2-server 14 | 15 | 16 | MIT License 17 | http://www.opensource.org/licenses/mit-license.php 18 | repo 19 | 20 | 21 | 22 | 23 | albert@myndocs.nl 24 | 25 | 26 | 27 | scm:git:https://github.com/myndocs/kotlin-oauth2-server.git 28 | scm:git:https://github.com/myndocs/kotlin-oauth2-server.git 29 | https://github.com/myndocs/kotlin-oauth2-server.git 30 | HEAD 31 | 32 | 33 | 34 | 1.3.31 35 | 1.8 36 | 1.8 37 | 1.6.21 38 | 39 | 40 | 41 | oauth2-server-core 42 | oauth2-server-json 43 | oauth2-server-integration-base 44 | oauth2-server-ktor 45 | oauth2-server-client-inmemory 46 | oauth2-server-identity-inmemory 47 | oauth2-server-token-store-inmemory 48 | oauth2-server-javalin 49 | oauth2-server-sparkjava 50 | oauth2-server-http4k 51 | oauth2-server-jwt 52 | oauth2-server-hexagon 53 | 54 | 55 | 56 | 57 | org.jetbrains.kotlin 58 | kotlin-stdlib-jdk8 59 | ${kotlin.version} 60 | provided 61 | 62 | 63 | org.jetbrains.kotlin 64 | kotlin-test 65 | ${kotlin.version} 66 | test 67 | 68 | 69 | 70 | org.junit.jupiter 71 | junit-jupiter-engine 72 | 5.2.0 73 | test 74 | 75 | 76 | io.mockk 77 | mockk 78 | 1.9.3 79 | test 80 | 81 | 82 | org.hamcrest 83 | hamcrest-library 84 | 1.3 85 | test 86 | 87 | 88 | org.slf4j 89 | slf4j-simple 90 | 1.7.25 91 | test 92 | 93 | 94 | 95 | 96 | 97 | 98 | org.jetbrains.kotlin 99 | kotlin-maven-plugin 100 | ${kotlin.version} 101 | 102 | 103 | compile 104 | compile 105 | 106 | compile 107 | 108 | 109 | 110 | test-compile 111 | test-compile 112 | 113 | test-compile 114 | 115 | 116 | 117 | 118 | 1.8 119 | 120 | 121 | 122 | org.apache.maven.plugins 123 | maven-source-plugin 124 | 3.2.0 125 | 126 | 127 | attach-sources 128 | 129 | jar 130 | 131 | 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-surefire-plugin 137 | 2.22.0 138 | 139 | 140 | 141 | 142 | 143 | 144 | release 145 | 146 | 147 | 148 | org.jetbrains.dokka 149 | dokka-maven-plugin 150 | ${dokka.version} 151 | 152 | 153 | prepare-package 154 | 155 | dokka 156 | javadoc 157 | javadocJar 158 | 159 | 160 | 161 | 162 | 163 | 164 | org.jetbrains.dokka 165 | kotlin-as-java-plugin 166 | ${dokka.version} 167 | 168 | 169 | 170 | 171 | 172 | org.apache.maven.plugins 173 | maven-javadoc-plugin 174 | 3.4.0 175 | 176 | 177 | attach-javadocs 178 | 179 | jar 180 | 181 | 182 | 183 | 184 | 185 | org.apache.maven.plugins 186 | maven-gpg-plugin 187 | 1.5 188 | 189 | 190 | sign-artifacts 191 | verify 192 | 193 | sign 194 | 195 | 196 | 197 | 198 | 199 | org.sonatype.plugins 200 | nexus-staging-maven-plugin 201 | 1.6.7 202 | true 203 | 204 | ossrh 205 | https://oss.sonatype.org 206 | true 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | ossrh 217 | https://oss.sonatype.org/service/local/staging/deploy/maven2 218 | 219 | 220 | -------------------------------------------------------------------------------- /oauth2-server-core/src/test/java/nl/myndocs/oauth2/PasswordGrantTokenServiceTest.kt: -------------------------------------------------------------------------------- 1 | package nl.myndocs.oauth2 2 | 3 | import io.mockk.every 4 | import io.mockk.impl.annotations.MockK 5 | import io.mockk.impl.annotations.RelaxedMockK 6 | import io.mockk.junit5.MockKExtension 7 | import io.mockk.verify 8 | import nl.myndocs.oauth2.client.AuthorizedGrantType 9 | import nl.myndocs.oauth2.client.Client 10 | import nl.myndocs.oauth2.client.ClientService 11 | import nl.myndocs.oauth2.exception.InvalidClientException 12 | import nl.myndocs.oauth2.exception.InvalidIdentityException 13 | import nl.myndocs.oauth2.exception.InvalidRequestException 14 | import nl.myndocs.oauth2.exception.InvalidScopeException 15 | import nl.myndocs.oauth2.grant.GrantingCall 16 | import nl.myndocs.oauth2.grant.authorize 17 | import nl.myndocs.oauth2.identity.Identity 18 | import nl.myndocs.oauth2.identity.IdentityService 19 | import nl.myndocs.oauth2.request.CallContext 20 | import nl.myndocs.oauth2.request.PasswordGrantRequest 21 | import nl.myndocs.oauth2.response.AccessTokenResponder 22 | import nl.myndocs.oauth2.token.AccessToken 23 | import nl.myndocs.oauth2.token.RefreshToken 24 | import nl.myndocs.oauth2.token.TokenStore 25 | import nl.myndocs.oauth2.token.converter.AccessTokenConverter 26 | import nl.myndocs.oauth2.token.converter.CodeTokenConverter 27 | import nl.myndocs.oauth2.token.converter.Converters 28 | import nl.myndocs.oauth2.token.converter.RefreshTokenConverter 29 | import org.junit.jupiter.api.Assertions.assertThrows 30 | import org.junit.jupiter.api.BeforeEach 31 | import org.junit.jupiter.api.Test 32 | import org.junit.jupiter.api.extension.ExtendWith 33 | import java.time.Instant 34 | 35 | @ExtendWith(MockKExtension::class) 36 | internal class PasswordGrantTokenServiceTest { 37 | @MockK 38 | lateinit var callContext: CallContext 39 | @MockK 40 | lateinit var identityService: IdentityService 41 | @MockK 42 | lateinit var clientService: ClientService 43 | @RelaxedMockK 44 | lateinit var tokenStore: TokenStore 45 | @MockK 46 | lateinit var accessTokenConverter: AccessTokenConverter 47 | @MockK 48 | lateinit var refreshTokenConverter: RefreshTokenConverter 49 | @MockK 50 | lateinit var codeTokenConverter: CodeTokenConverter 51 | @MockK 52 | lateinit var accessTokenResponder: AccessTokenResponder 53 | 54 | lateinit var grantingCall: GrantingCall 55 | 56 | @BeforeEach 57 | fun initialize() { 58 | grantingCall = object : GrantingCall { 59 | override val callContext = this@PasswordGrantTokenServiceTest.callContext 60 | override val identityService = this@PasswordGrantTokenServiceTest.identityService 61 | override val clientService = this@PasswordGrantTokenServiceTest.clientService 62 | override val tokenStore = this@PasswordGrantTokenServiceTest.tokenStore 63 | override val converters = Converters( 64 | this@PasswordGrantTokenServiceTest.accessTokenConverter, 65 | this@PasswordGrantTokenServiceTest.refreshTokenConverter, 66 | this@PasswordGrantTokenServiceTest.codeTokenConverter 67 | ) 68 | override val accessTokenResponder = this@PasswordGrantTokenServiceTest.accessTokenResponder 69 | } 70 | } 71 | val clientId = "client-foo" 72 | val clientSecret = "client-bar" 73 | val username = "user-foo" 74 | val password = "password-bar" 75 | val scope = "scope1" 76 | val scopes = setOf(scope) 77 | 78 | val passwordGrantRequest = PasswordGrantRequest( 79 | clientId, 80 | clientSecret, 81 | username, 82 | password, 83 | scope 84 | ) 85 | 86 | @Test 87 | fun validPasswordGrant() { 88 | val client = Client(clientId, setOf("scope1", "scope2"), setOf(), setOf(AuthorizedGrantType.PASSWORD)) 89 | val identity = Identity(username) 90 | val requestScopes = setOf("scope1") 91 | val refreshToken = RefreshToken("test", Instant.now(), identity, clientId, requestScopes) 92 | val accessToken = AccessToken("test", "bearer", Instant.now(), identity, clientId, requestScopes, refreshToken) 93 | 94 | every { clientService.clientOf(clientId) } returns client 95 | every { clientService.validClient(client, clientSecret) } returns true 96 | every { identityService.identityOf(client, username) } returns identity 97 | every { identityService.validCredentials(client, identity, password) } returns true 98 | every { identityService.allowedScopes(client, identity, requestScopes) } returns scopes 99 | every { refreshTokenConverter.convertToToken(identity, clientId, requestScopes) } returns refreshToken 100 | every { accessTokenConverter.convertToToken(identity, clientId, requestScopes, refreshToken) } returns accessToken 101 | 102 | grantingCall.authorize(passwordGrantRequest) 103 | 104 | verify { tokenStore.storeAccessToken(accessToken) } 105 | } 106 | 107 | @Test 108 | fun nonExistingClientException() { 109 | every { clientService.clientOf(clientId) } returns null 110 | 111 | assertThrows( 112 | InvalidClientException::class.java 113 | ) { grantingCall.authorize(passwordGrantRequest) } 114 | } 115 | 116 | @Test 117 | fun invalidClientException() { 118 | val client = Client(clientId, setOf(), setOf(), setOf(AuthorizedGrantType.PASSWORD)) 119 | every { clientService.clientOf(clientId) } returns client 120 | every { clientService.validClient(client, clientSecret) } returns false 121 | 122 | assertThrows( 123 | InvalidClientException::class.java 124 | ) { grantingCall.authorize(passwordGrantRequest) } 125 | } 126 | 127 | @Test 128 | fun missingUsernameException() { 129 | val passwordGrantRequest = PasswordGrantRequest( 130 | clientId, 131 | clientSecret, 132 | null, 133 | password, 134 | scope 135 | ) 136 | 137 | val client = Client(clientId, setOf(), setOf(), setOf(AuthorizedGrantType.PASSWORD)) 138 | every { clientService.clientOf(clientId) } returns client 139 | every { clientService.validClient(client, clientSecret) } returns true 140 | 141 | assertThrows( 142 | InvalidRequestException::class.java 143 | ) { grantingCall.authorize(passwordGrantRequest) } 144 | } 145 | 146 | @Test 147 | fun missingPasswordException() { 148 | val passwordGrantRequest = PasswordGrantRequest( 149 | clientId, 150 | clientSecret, 151 | username, 152 | null, 153 | scope 154 | ) 155 | 156 | val client = Client(clientId, setOf(), setOf(), setOf(AuthorizedGrantType.PASSWORD)) 157 | every { clientService.clientOf(clientId) } returns client 158 | every { clientService.validClient(client, clientSecret) } returns true 159 | 160 | assertThrows( 161 | InvalidRequestException::class.java 162 | ) { grantingCall.authorize(passwordGrantRequest) } 163 | } 164 | 165 | @Test 166 | fun invalidIdentityException() { 167 | val client = Client(clientId, setOf(), setOf(), setOf(AuthorizedGrantType.PASSWORD)) 168 | val identity = Identity(username) 169 | 170 | every { clientService.clientOf(clientId) } returns client 171 | every { clientService.validClient(client, clientSecret) } returns true 172 | every { identityService.identityOf(client, username) } returns identity 173 | every { identityService.validCredentials(client, identity, password) } returns false 174 | 175 | assertThrows( 176 | InvalidIdentityException::class.java 177 | ) { grantingCall.authorize(passwordGrantRequest) } 178 | } 179 | 180 | @Test 181 | fun invalidIdentityScopeException() { 182 | val client = Client(clientId, setOf("scope1", "scope2"), setOf(), setOf(AuthorizedGrantType.PASSWORD)) 183 | val identity = Identity(username) 184 | 185 | every { clientService.clientOf(clientId) } returns client 186 | every { clientService.validClient(client, clientSecret) } returns true 187 | every { identityService.identityOf(client, username) } returns identity 188 | every { identityService.validCredentials(client, identity, password) } returns true 189 | every { identityService.allowedScopes(client, identity, scopes) } returns setOf() 190 | 191 | assertThrows( 192 | InvalidScopeException::class.java 193 | ) { grantingCall.authorize(passwordGrantRequest) } 194 | } 195 | 196 | @Test 197 | fun invalidRequestClientScopeException() { 198 | val client = Client(clientId, setOf("scope3"), setOf(), setOf(AuthorizedGrantType.PASSWORD)) 199 | val identity = Identity(username) 200 | 201 | every { clientService.clientOf(clientId) } returns client 202 | every { clientService.validClient(client, clientSecret) } returns true 203 | every { identityService.identityOf(client, username) } returns identity 204 | every { identityService.validCredentials(client, identity, password) } returns true 205 | every { identityService.allowedScopes(client, identity, scopes) } returns scopes 206 | 207 | assertThrows( 208 | InvalidScopeException::class.java 209 | ) { grantingCall.authorize(passwordGrantRequest) } 210 | } 211 | 212 | @Test 213 | fun clientScopesAsFallback() { 214 | val passwordGrantRequest = PasswordGrantRequest( 215 | clientId, 216 | clientSecret, 217 | username, 218 | password, 219 | null 220 | ) 221 | 222 | val client = Client(clientId, setOf("scope1", "scope2"), setOf(), setOf(AuthorizedGrantType.PASSWORD)) 223 | val identity = Identity(username) 224 | val requestScopes = setOf("scope1", "scope2") 225 | val refreshToken = RefreshToken("test", Instant.now(), identity, clientId, requestScopes) 226 | val accessToken = AccessToken("test", "bearer", Instant.now(), identity, clientId, requestScopes, refreshToken) 227 | 228 | every { clientService.clientOf(clientId) } returns client 229 | every { clientService.validClient(client, clientSecret) } returns true 230 | every { identityService.identityOf(client, username) } returns identity 231 | every { identityService.validCredentials(client, identity, password) } returns true 232 | every { identityService.allowedScopes(client, identity, requestScopes) } returns requestScopes 233 | every { refreshTokenConverter.convertToToken(identity, clientId, requestScopes) } returns refreshToken 234 | every { accessTokenConverter.convertToToken(identity, clientId, requestScopes, refreshToken) } returns accessToken 235 | 236 | grantingCall.authorize(passwordGrantRequest) 237 | } 238 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. --------------------------------------------------------------------------------