diff --git a/kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt b/kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt new file mode 100644 index 0000000..fb9bf9d --- /dev/null +++ b/kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/CallRouter.kt @@ -0,0 +1,214 @@ +package nl.myndocs.oauth2 + +import nl.myndocs.oauth2.authenticator.Authorizer +import nl.myndocs.oauth2.exception.* +import nl.myndocs.oauth2.identity.UserInfo +import nl.myndocs.oauth2.request.* +import nl.myndocs.oauth2.token.toMap + +class CallRouter( + private val tokenService: TokenService, + private val tokenEndpoint: String, + private val authorizeEndpoint: String, + private val userinfoEndpoint: String, + private val userInfoCallback: (UserInfo) -> Map +) { + companion object { + const val METHOD_POST = "post" + const val METHOD_GET = "get" + + const val STATUS_BAD_REQUEST = 400 + const val STATUS_UNAUTHORIZED = 401 + + } + + fun route( + callContext: CallContext, + authorizer: Authorizer) { + when (callContext.path) { + tokenEndpoint -> routeTokenEndpoint(callContext) + authorizeEndpoint -> routeAuthorizeEndpoint(callContext, authorizer) + userinfoEndpoint -> routeUserInfoEndpoint(callContext) + } + } + + private fun routeTokenEndpoint(callContext: CallContext) { + if (callContext.method.toLowerCase() != METHOD_POST) { + return + } + + try { + val allowedGrantTypes = setOf("password", "authorization_code", "refresh_token") + val grantType = callContext.formParameters["grant_type"] + ?: throw InvalidRequestException("'grant_type' not given") + + if (!allowedGrantTypes.contains(grantType)) { + throw InvalidGrantException("'grant_type' with value '$grantType' not allowed") + } + + when (grantType) { + "password" -> routePasswordGrant(callContext, tokenService) + "authorization_code" -> routeAuthorizationCodeGrant(callContext, tokenService) + "refresh_token" -> routeRefreshTokenGrant(callContext, tokenService) + } + } catch (oauthException: OauthException) { + callContext.respondStatus(STATUS_BAD_REQUEST) + callContext.respondJson(oauthException.toMap()) + } + } + + fun routePasswordGrant(callContext: CallContext, tokenService: TokenService) { + val tokenResponse = tokenService.authorize( + PasswordGrantRequest( + callContext.formParameters["client_id"], + callContext.formParameters["client_secret"], + callContext.formParameters["username"], + callContext.formParameters["password"], + callContext.formParameters["scope"] + ) + ) + + callContext.respondJson(tokenResponse.toMap()) + } + + fun routeRefreshTokenGrant(callContext: CallContext, tokenService: TokenService) { + val accessToken = tokenService.refresh( + RefreshTokenRequest( + callContext.formParameters["client_id"], + callContext.formParameters["client_secret"], + callContext.formParameters["refresh_token"] + ) + ) + + callContext.respondJson(accessToken.toMap()) + } + + fun routeAuthorizationCodeGrant(callContext: CallContext, tokenService: TokenService) { + val accessToken = tokenService.authorize( + AuthorizationCodeRequest( + callContext.formParameters["client_id"], + callContext.formParameters["client_secret"], + callContext.formParameters["code"], + callContext.formParameters["redirect_uri"] + ) + ) + + callContext.respondJson(accessToken.toMap()) + } + + + fun routeAuthorizationCodeRedirect( + callContext: CallContext, + tokenService: TokenService, + authorizer: Authorizer + ) { + val queryParameters = callContext.queryParameters + val credentials = authorizer.extractCredentials() + try { + val redirect = tokenService.redirect( + RedirectAuthorizationCodeRequest( + queryParameters["client_id"], + queryParameters["redirect_uri"], + credentials?.username ?: "", + credentials?.password ?: "", + queryParameters["scope"] + ), + authorizer.authenticator(), + authorizer.scopesVerifier() + ) + + var stateQueryParameter = "" + + if (queryParameters["state"] != null) { + stateQueryParameter = "&state=" + queryParameters["state"] + } + + callContext.redirect(queryParameters["redirect_uri"] + "?code=${redirect.codeToken}$stateQueryParameter") + } catch (unverifiedIdentityException: InvalidIdentityException) { + authorizer.failedAuthentication() + callContext.respondStatus(STATUS_UNAUTHORIZED) + } + } + + + fun routeAccessTokenRedirect( + callContext: CallContext, + tokenService: TokenService, + authorizer: Authorizer + ) { + val queryParameters = callContext.queryParameters + val credentials = authorizer.extractCredentials() + + try { + val redirect = tokenService.redirect( + RedirectTokenRequest( + queryParameters["client_id"], + queryParameters["redirect_uri"], + credentials?.username ?: "", + credentials?.password ?: "", + queryParameters["scope"] + ), + authorizer.authenticator(), + authorizer.scopesVerifier() + ) + + var stateQueryParameter = "" + + if (queryParameters["state"] != null) { + stateQueryParameter = "&state=" + queryParameters["state"] + } + + callContext.redirect( + queryParameters["redirect_uri"] + "#access_token=${redirect.accessToken}" + + "&token_type=bearer&expires_in=${redirect.expiresIn()}$stateQueryParameter" + ) + + } catch (unverifiedIdentityException: InvalidIdentityException) { + authorizer.failedAuthentication() + callContext.respondStatus(STATUS_UNAUTHORIZED) + } + } + + private fun routeAuthorizeEndpoint(callContext: CallContext, authorizer: Authorizer) { + try { + if (callContext.method.toLowerCase() != METHOD_GET) { + return + } + + val allowedResponseTypes = setOf("code", "token") + val responseType = callContext.queryParameters["response_type"] + ?: throw InvalidRequestException("'response_type' not given") + + if (!allowedResponseTypes.contains(responseType)) { + throw InvalidGrantException("'grant_type' with value '$responseType' not allowed") + } + + when (responseType) { + "code" -> routeAuthorizationCodeRedirect(callContext, tokenService, authorizer) + "token" -> routeAccessTokenRedirect(callContext, tokenService, authorizer) + } + } catch (oauthException: OauthException) { + callContext.respondStatus(STATUS_BAD_REQUEST) + callContext.respondJson(oauthException.toMap()) + } + } + + private fun routeUserInfoEndpoint(callContext: CallContext) { + if (callContext.method.toLowerCase() != METHOD_GET) { + return + } + + val authorization = callContext.headers["Authorization"] + + if (authorization == null || !authorization.startsWith("bearer ", true)) { + callContext.respondStatus(STATUS_UNAUTHORIZED) + return + } + + val token = authorization.substring(7) + + val userInfoCallback = userInfoCallback(tokenService.userInfo(token)) + + callContext.respondJson(userInfoCallback) + } +} \ No newline at end of file diff --git a/kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/CallContext.kt b/kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/CallContext.kt new file mode 100644 index 0000000..875f3b3 --- /dev/null +++ b/kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/CallContext.kt @@ -0,0 +1,14 @@ +package nl.myndocs.oauth2.request + +interface CallContext { + val path: String + val method: String + val headers: Map + val queryParameters: Map + val formParameters: Map + + fun respondStatus(statusCode: Int) + fun respondHeader(name: String, value: String) + fun respondJson(content: Any) + fun redirect(uri: String) +} \ No newline at end of file diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/BasicAuth.kt b/kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/auth/BasicAuth.kt similarity index 72% rename from kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/BasicAuth.kt rename to kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/auth/BasicAuth.kt index 49de703..560ac3c 100644 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/BasicAuth.kt +++ b/kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/auth/BasicAuth.kt @@ -1,14 +1,8 @@ -package nl.myndocs.oauth2.ktor.feature.util +package nl.myndocs.oauth2.request.auth import java.util.* object BasicAuth { - @Deprecated("Removed with 0.2.0") - fun parse(authorization: String): Credentials { - val parseCredentials = parseCredentials(authorization) - return Credentials(parseCredentials.username, parseCredentials.password) - } - fun parseCredentials(authorization: String): nl.myndocs.oauth2.authenticator.Credentials { var username: String? = null var password: String? = null diff --git a/kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/auth/BasicAuthorizer.kt b/kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/auth/BasicAuthorizer.kt new file mode 100644 index 0000000..b0534ae --- /dev/null +++ b/kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/request/auth/BasicAuthorizer.kt @@ -0,0 +1,16 @@ +package nl.myndocs.oauth2.request.auth + +import nl.myndocs.oauth2.authenticator.Authorizer +import nl.myndocs.oauth2.authenticator.Credentials +import nl.myndocs.oauth2.request.CallContext + +open class BasicAuthorizer(protected val context: CallContext) : Authorizer { + override fun extractCredentials(): Credentials? { + val authorizationHeader = context.headers["authorization"] ?: "" + return BasicAuth.parseCredentials(authorizationHeader) + } + + override fun failedAuthentication() { + context.respondHeader("WWW-Authenticate", "Basic realm=\"${context.queryParameters["client_id"]}\"") + } +} \ No newline at end of file diff --git a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/Oauth2Server.kt b/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/Oauth2Server.kt index b22e53b..954bcb5 100644 --- a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/Oauth2Server.kt +++ b/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/Oauth2Server.kt @@ -1,19 +1,16 @@ package nl.myndocs.oauth2.javalin -import io.javalin.Context import io.javalin.Javalin import io.javalin.apibuilder.ApiBuilder.* +import nl.myndocs.oauth2.CallRouter import nl.myndocs.oauth2.TokenService import nl.myndocs.oauth2.authenticator.Authorizer import nl.myndocs.oauth2.client.ClientService -import nl.myndocs.oauth2.exception.InvalidGrantException -import nl.myndocs.oauth2.exception.InvalidRequestException -import nl.myndocs.oauth2.exception.OauthException -import nl.myndocs.oauth2.exception.toMap import nl.myndocs.oauth2.identity.IdentityService import nl.myndocs.oauth2.identity.UserInfo -import nl.myndocs.oauth2.javalin.routing.* -import nl.myndocs.oauth2.javalin.util.BasicAuthorizer +import nl.myndocs.oauth2.javalin.request.JavalinCallContext +import nl.myndocs.oauth2.request.CallContext +import nl.myndocs.oauth2.request.auth.BasicAuthorizer import nl.myndocs.oauth2.token.TokenStore import nl.myndocs.oauth2.token.converter.* @@ -33,7 +30,7 @@ data class OauthConfiguration( "scopes" to userInfo.scopes ) }, - var authorizerFactory: (Context) -> Authorizer = ::BasicAuthorizer + var authorizerFactory: (CallContext) -> Authorizer = ::BasicAuthorizer ) fun Javalin.enableOauthServer(configurationCallback: OauthConfiguration.() -> Unit) { @@ -49,78 +46,33 @@ fun Javalin.enableOauthServer(configurationCallback: OauthConfiguration.() -> Un configuration.codeTokenConverter ) + val callRouter = CallRouter( + tokenService, + configuration.tokenEndpoint, + configuration.authorizeEndpoint, + configuration.userInfoEndpoint, + configuration.userInfoCallback + ) + this.routes { path(configuration.tokenEndpoint) { post { ctx -> - try { - val allowedGrantTypes = setOf("password", "authorization_code", "refresh_token") - val grantType = ctx.formParam("grant_type") - ?: throw InvalidRequestException("'grant_type' not given") - - if (!allowedGrantTypes.contains(grantType)) { - throw InvalidGrantException("'grant_type' with value '$grantType' not allowed") - } - - val paramMap = ctx.formParamMap() - .mapValues { ctx.formParam(it.key) } - - when (grantType) { - "password" -> routePasswordGrant(ctx, tokenService, paramMap) - "authorization_code" -> routeAuthorizationCodeGrant(ctx, tokenService, paramMap) - "refresh_token" -> routeRefreshTokenGrant(ctx, tokenService, paramMap) - } - } catch (oauthException: OauthException) { - ctx.status(400) - ctx.json(oauthException.toMap()) - } - + val javalinCallContext = JavalinCallContext(ctx) + callRouter.route(javalinCallContext, configuration.authorizerFactory(javalinCallContext)) } } path(configuration.authorizeEndpoint) { get { ctx -> - try { - val allowedResponseTypes = setOf("code", "token") - val responseType = ctx.queryParam("response_type") - ?: throw InvalidRequestException("'response_type' not given") - - if (!allowedResponseTypes.contains(responseType)) { - throw InvalidGrantException("'grant_type' with value '$responseType' not allowed") - } - - val paramMap = ctx.queryParamMap() - .mapValues { ctx.queryParam(it.key) } - - when (responseType) { - "code" -> routeAuthorizationCodeRedirect(ctx, tokenService, paramMap, configuration.authorizerFactory) - "token" -> routeAccessTokenRedirect(ctx, tokenService, paramMap, configuration.authorizerFactory) - } - } catch (oauthException: OauthException) { - ctx.status(400) - ctx.json(oauthException.toMap()) - } + val javalinCallContext = JavalinCallContext(ctx) + callRouter.route(javalinCallContext, configuration.authorizerFactory(javalinCallContext)) } } path(configuration.userInfoEndpoint) { get { ctx -> - val authorization = ctx.header("Authorization") - - if (authorization == null) { - ctx.status(401) - return@get - } - - if (!authorization.startsWith("bearer ", true)) { - ctx.status(401) - return@get - } - - val token = authorization.substring(7) - - val userInfoCallback = configuration.userInfoCallback(tokenService.userInfo(token)) - - ctx.json(userInfoCallback) + val javalinCallContext = JavalinCallContext(ctx) + callRouter.route(javalinCallContext, configuration.authorizerFactory(javalinCallContext)) } } } diff --git a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/request/JavalinCallContext.kt b/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/request/JavalinCallContext.kt new file mode 100644 index 0000000..f08b918 --- /dev/null +++ b/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/request/JavalinCallContext.kt @@ -0,0 +1,35 @@ +package nl.myndocs.oauth2.javalin.request + +import io.javalin.Context +import nl.myndocs.oauth2.request.CallContext + +class JavalinCallContext(val context: Context) : CallContext { + override val path: String = context.path() + override val method: String = context.method() + override val headers: Map = context.headerMap() + override val queryParameters: Map = context.queryParamMap() + .mapValues { context.queryParam(it.key) } + .filterValues { it != null } + .mapValues { it.value!! } + + override val formParameters: Map = context.formParamMap() + .mapValues { context.formParam(it.key) } + .filterValues { it != null } + .mapValues { it.value!! } + + override fun respondStatus(statusCode: Int) { + context.status(statusCode) + } + + override fun respondHeader(name: String, value: String) { + context.header(name, value) + } + + override fun respondJson(content: Any) { + context.json(content) + } + + override fun redirect(uri: String) { + context.redirect(uri) + } +} \ No newline at end of file diff --git a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/routing/AuthorizeRouting.kt b/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/routing/AuthorizeRouting.kt deleted file mode 100644 index df2da26..0000000 --- a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/routing/AuthorizeRouting.kt +++ /dev/null @@ -1,84 +0,0 @@ -package nl.myndocs.oauth2.javalin.routing - -import io.javalin.Context -import nl.myndocs.oauth2.TokenService -import nl.myndocs.oauth2.authenticator.Authorizer -import nl.myndocs.oauth2.exception.InvalidIdentityException -import nl.myndocs.oauth2.request.RedirectAuthorizationCodeRequest -import nl.myndocs.oauth2.request.RedirectTokenRequest - - -fun routeAuthorizationCodeRedirect( - ctx: Context, - tokenService: TokenService, - queryParameters: Map, - authorizerFactory: (Context) -> Authorizer -) { - val authorizer = authorizerFactory(ctx) - val credentials = authorizer.extractCredentials() - try { - val redirect = tokenService.redirect( - RedirectAuthorizationCodeRequest( - queryParameters["client_id"], - queryParameters["redirect_uri"], - credentials?.username ?: "", - credentials?.password ?: "", - queryParameters["scope"] - ), - authorizer.authenticator(), - authorizer.scopesVerifier() - ) - - var stateQueryParameter = "" - - if (queryParameters["state"] != null) { - stateQueryParameter = "&state=" + queryParameters["state"] - } - - ctx.redirect(queryParameters["redirect_uri"] + "?code=${redirect.codeToken}$stateQueryParameter") - } catch (unverifiedIdentityException: InvalidIdentityException) { - authorizer.failedAuthentication() - ctx.status(401) - } -} - - -fun routeAccessTokenRedirect( - ctx: Context, - tokenService: TokenService, - queryParameters: - Map, - authorizerFactory: (Context) -> Authorizer -) { - val authorizer = authorizerFactory(ctx) - val credentials = authorizer.extractCredentials() - - try { - val redirect = tokenService.redirect( - RedirectTokenRequest( - queryParameters["client_id"], - queryParameters["redirect_uri"], - credentials?.username ?: "", - credentials?.password ?: "", - queryParameters["scope"] - ), - authorizer.authenticator(), - authorizer.scopesVerifier() - ) - - var stateQueryParameter = "" - - if (queryParameters["state"] != null) { - stateQueryParameter = "&state=" + queryParameters["state"] - } - - ctx.redirect( - queryParameters["redirect_uri"] + "#access_token=${redirect.accessToken}" + - "&token_type=bearer&expires_in=${redirect.expiresIn()}$stateQueryParameter" - ) - - } catch (unverifiedIdentityException: InvalidIdentityException) { - authorizer.failedAuthentication() - ctx.status(401) - } -} \ No newline at end of file diff --git a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/routing/TokenRouting.kt b/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/routing/TokenRouting.kt deleted file mode 100644 index 0e14ed6..0000000 --- a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/routing/TokenRouting.kt +++ /dev/null @@ -1,48 +0,0 @@ -package nl.myndocs.oauth2.javalin.routing - -import io.javalin.Context -import nl.myndocs.oauth2.TokenService -import nl.myndocs.oauth2.request.AuthorizationCodeRequest -import nl.myndocs.oauth2.request.PasswordGrantRequest -import nl.myndocs.oauth2.request.RefreshTokenRequest -import nl.myndocs.oauth2.token.toMap - - -fun routePasswordGrant(ctx: Context, tokenService: TokenService, formParams: Map) { - val tokenResponse = tokenService.authorize( - PasswordGrantRequest( - formParams["client_id"], - formParams["client_secret"], - formParams["username"], - formParams["password"], - formParams["scope"] - ) - ) - - ctx.json(tokenResponse.toMap()) -} - -fun routeRefreshTokenGrant(ctx: Context, tokenService: TokenService, formParams: Map) { - val accessToken = tokenService.refresh( - RefreshTokenRequest( - formParams["client_id"], - formParams["client_secret"], - formParams["refresh_token"] - ) - ) - - ctx.json(accessToken.toMap()) -} - -fun routeAuthorizationCodeGrant(ctx: Context, tokenService: TokenService, formParams: Map) { - val accessToken = tokenService.authorize( - AuthorizationCodeRequest( - formParams["client_id"], - formParams["client_secret"], - formParams["code"], - formParams["redirect_uri"] - ) - ) - - ctx.json(accessToken.toMap()) -} diff --git a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/util/BasicAuth.kt b/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/util/BasicAuth.kt deleted file mode 100644 index 7f032e1..0000000 --- a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/util/BasicAuth.kt +++ /dev/null @@ -1,34 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.util - -import nl.myndocs.oauth2.javalin.util.Credentials -import java.util.* - -object BasicAuth { - @Deprecated("Removed in 0.2.0") - fun parse(authorization: String): Credentials { - val parseCredentials = parseCredentials(authorization) - return Credentials(parseCredentials.username, parseCredentials.password) - } - - fun parseCredentials(authorization: String): nl.myndocs.oauth2.authenticator.Credentials { - var username: String? = null - var password: String? = null - - if (authorization.startsWith("basic ", true)) { - - val basicAuthorizationString = String( - Base64.getDecoder() - .decode(authorization.substring(6)) - ) - - val splittedString = basicAuthorizationString.split(":") - - if (splittedString.size == 2) { - username = splittedString[0] - password = splittedString[1] - } - } - - return nl.myndocs.oauth2.authenticator.Credentials(username, password) - } -} \ No newline at end of file diff --git a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/util/BasicAuthorizer.kt b/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/util/BasicAuthorizer.kt deleted file mode 100644 index beb033e..0000000 --- a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/util/BasicAuthorizer.kt +++ /dev/null @@ -1,17 +0,0 @@ -package nl.myndocs.oauth2.javalin.util - -import io.javalin.Context -import nl.myndocs.oauth2.authenticator.Authorizer -import nl.myndocs.oauth2.authenticator.Credentials -import nl.myndocs.oauth2.ktor.feature.util.BasicAuth - -open class BasicAuthorizer(protected val context: Context) : Authorizer { - override fun extractCredentials(): Credentials? { - val authorizationHeader = context.header("authorization") ?: "" - return BasicAuth.parseCredentials(authorizationHeader) - } - - override fun failedAuthentication() { - context.header("WWW-Authenticate", "Basic realm=\"${context.queryParam("client_id")}\"") - } -} \ No newline at end of file diff --git a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/util/Credentials.kt b/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/util/Credentials.kt deleted file mode 100644 index d2c8cc1..0000000 --- a/kotlin-oauth2-server-javalin/src/main/java/nl/myndocs/oauth2/javalin/util/Credentials.kt +++ /dev/null @@ -1,4 +0,0 @@ -package nl.myndocs.oauth2.javalin.util - -@Deprecated("Replaced by core Credentials removed in 0.2.0") -data class Credentials(val username: String?, val password: String?) \ No newline at end of file diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/Oauth2ServerFeature.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/Oauth2ServerFeature.kt index be1205f..350cc86 100644 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/Oauth2ServerFeature.kt +++ b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/Oauth2ServerFeature.kt @@ -1,25 +1,18 @@ package nl.myndocs.oauth2.ktor.feature -import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCallPipeline import io.ktor.application.ApplicationFeature import io.ktor.application.call -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.request.header -import io.ktor.request.httpMethod -import io.ktor.request.path -import io.ktor.response.respond import io.ktor.util.AttributeKey +import nl.myndocs.oauth2.CallRouter import nl.myndocs.oauth2.TokenService import nl.myndocs.oauth2.authenticator.Authorizer import nl.myndocs.oauth2.client.ClientService import nl.myndocs.oauth2.identity.IdentityService import nl.myndocs.oauth2.identity.UserInfo -import nl.myndocs.oauth2.ktor.feature.json.MapToJson -import nl.myndocs.oauth2.ktor.feature.routing.authorize.configureAuthorizeEndpoint -import nl.myndocs.oauth2.ktor.feature.routing.token.configureTokenEndpoint -import nl.myndocs.oauth2.ktor.feature.util.BasicAuthorizer +import nl.myndocs.oauth2.ktor.feature.request.KtorCallContext +import nl.myndocs.oauth2.request.CallContext +import nl.myndocs.oauth2.request.auth.BasicAuthorizer import nl.myndocs.oauth2.token.TokenStore import nl.myndocs.oauth2.token.converter.* @@ -42,7 +35,14 @@ class Oauth2ServerFeature(configuration: Configuration) { refreshTokenConverter, codeTokenConverter ) - val authorizerFactory: (ApplicationCall) -> Authorizer = configuration.authorizerFactory + val callRouter = CallRouter( + tokenService, + tokenEndpoint, + authorizeEndpoint, + userInfoEndpoint, + userInfoCallback + ) + val authorizerFactory: (CallContext) -> Authorizer = configuration.authorizerFactory class Configuration { var tokenEndpoint = "/oauth/token" @@ -60,7 +60,7 @@ class Oauth2ServerFeature(configuration: Configuration) { "scopes" to userInfo.scopes ) } - var authorizerFactory: (ApplicationCall) -> Authorizer = ::BasicAuthorizer + var authorizerFactory: (CallContext) -> Authorizer = ::BasicAuthorizer } companion object Feature : ApplicationFeature { @@ -73,41 +73,10 @@ class Oauth2ServerFeature(configuration: Configuration) { val feature = Oauth2ServerFeature(configuration) pipeline.intercept(ApplicationCallPipeline.Infrastructure) { - configureTokenEndpoint(feature) - configureAuthorizeEndpoint(feature) - - if (call.request.httpMethod != HttpMethod.Get) { - proceed() - return@intercept - } - - val requestPath = call.request.path() - if (requestPath != feature.userInfoEndpoint) { - proceed() - return@intercept - } - - val authorization = call.request.header("Authorization") - - if (authorization == null) { - call.respond(HttpStatusCode.Unauthorized) - finish() - return@intercept - } - - if (!authorization.startsWith("bearer ", true)) { - call.respond(HttpStatusCode.Unauthorized) - finish() - return@intercept - } - - val token = authorization.substring(7) - - val userInfoCallback = feature.userInfoCallback(feature.tokenService.userInfo(token)) + val ktorCallContext = KtorCallContext(call) + val authorizer = feature.authorizerFactory(ktorCallContext) - call.respond(MapToJson.toJson(userInfoCallback)) - finish() - return@intercept + feature.callRouter.route(ktorCallContext, authorizer) } return feature diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/json/MapToJson.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/json/JsonMapper.kt similarity index 57% rename from kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/json/MapToJson.kt rename to kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/json/JsonMapper.kt index 80c0dca..ef1f113 100644 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/json/MapToJson.kt +++ b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/json/JsonMapper.kt @@ -2,8 +2,8 @@ package nl.myndocs.oauth2.ktor.feature.json import com.google.gson.Gson -object MapToJson { +object JsonMapper { private val gson = Gson() - fun toJson(map: Map) = gson.toJson(map) + fun toJson(content: Any) = gson.toJson(content) } \ No newline at end of file diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/request/KtorCallContext.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/request/KtorCallContext.kt new file mode 100644 index 0000000..f117158 --- /dev/null +++ b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/request/KtorCallContext.kt @@ -0,0 +1,72 @@ +package nl.myndocs.oauth2.ktor.feature.request + +import io.ktor.application.ApplicationCall +import io.ktor.http.HttpStatusCode +import io.ktor.request.header +import io.ktor.request.httpMethod +import io.ktor.request.path +import io.ktor.request.receiveParameters +import io.ktor.response.header +import io.ktor.response.respondRedirect +import io.ktor.response.respondText +import io.ktor.util.toMap +import kotlinx.coroutines.experimental.runBlocking +import nl.myndocs.oauth2.ktor.feature.json.JsonMapper +import nl.myndocs.oauth2.request.CallContext + +class KtorCallContext(val applicationCall: ApplicationCall) : CallContext { + override val path: String = applicationCall.request.path() + override val method: String = applicationCall.request.httpMethod.value + override val headers: Map = applicationCall.request + .headers + .toMap() + .mapValues { applicationCall.request.header(it.key) } + .filterValues { it != null } + .mapValues { it.value!! } + + override val queryParameters: Map = applicationCall.request + .queryParameters + .toMap() + .filterValues { it.isNotEmpty() } + .mapValues { it.value.first() } + + private var _formParameters: Map? = null + override val formParameters: Map + get() = receiveParameters() + + private fun receiveParameters(): Map { + if (_formParameters == null) { + _formParameters = runBlocking { + applicationCall.receiveParameters() + .toMap() + .filterValues { it.isNotEmpty() } + .mapValues { it.value.first() } + } + } + + return _formParameters!! + } + + override fun respondStatus(statusCode: Int) { + applicationCall.response.status(HttpStatusCode.fromValue(statusCode)) + } + + override fun respondHeader(name: String, value: String) { + applicationCall.response.header(name, value) + } + + override fun respondJson(content: Any) { + runBlocking { + applicationCall.respondText( + JsonMapper.toJson(content), + io.ktor.http.ContentType.Application.Json + ) + } + } + + override fun redirect(uri: String) { + runBlocking { + applicationCall.respondRedirect(uri) + } + } +} \ No newline at end of file diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/authorize/AuthorizationCodeRouting.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/authorize/AuthorizationCodeRouting.kt deleted file mode 100644 index eee5b70..0000000 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/authorize/AuthorizationCodeRouting.kt +++ /dev/null @@ -1,73 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.routing.authorize - -import io.ktor.application.ApplicationCall -import io.ktor.application.call -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.pipeline.PipelineContext -import io.ktor.request.httpMethod -import io.ktor.request.path -import io.ktor.response.respond -import io.ktor.response.respondRedirect -import io.ktor.response.respondText -import nl.myndocs.oauth2.exception.InvalidIdentityException -import nl.myndocs.oauth2.exception.OauthException -import nl.myndocs.oauth2.ktor.feature.Oauth2ServerFeature -import nl.myndocs.oauth2.ktor.feature.util.toJson -import nl.myndocs.oauth2.request.RedirectAuthorizationCodeRequest - -suspend fun PipelineContext.configureAuthorizationCodeGranting(feature: Oauth2ServerFeature) { - if (call.request.httpMethod != HttpMethod.Get) { - proceed() - return - } - - val requestPath = call.request.path() - if (requestPath != feature.authorizeEndpoint) { - proceed() - return - } - - val queryParameters = call.request.queryParameters - - val authorizer = feature.authorizerFactory(call) - val credentials = authorizer.extractCredentials() - - try { - val redirect = feature.tokenService.redirect( - RedirectAuthorizationCodeRequest( - queryParameters["client_id"], - queryParameters["redirect_uri"], - credentials?.username ?: "", - credentials?.password ?: "", - queryParameters["scope"] - ), - authorizer.authenticator(), - authorizer.scopesVerifier() - ) - - var stateQueryParameter = "" - - if (queryParameters["state"] != null) { - stateQueryParameter = "&state=" + queryParameters["state"] - } - - - call.respondRedirect( - queryParameters["redirect_uri"] + "?code=${redirect.codeToken}$stateQueryParameter" - ) - - finish() - return - } catch (unverifiedIdentityException: InvalidIdentityException) { - authorizer.failedAuthentication() - call.respond(HttpStatusCode.Unauthorized) - finish() - return - } catch (oauthException: OauthException) { - call.respondText(text = oauthException.toJson(), status = HttpStatusCode.BadRequest) - finish() - return - } -} - diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/authorize/AuthorizeEndpointRouting.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/authorize/AuthorizeEndpointRouting.kt deleted file mode 100644 index 9e84bb5..0000000 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/authorize/AuthorizeEndpointRouting.kt +++ /dev/null @@ -1,52 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.routing.authorize - -import io.ktor.application.ApplicationCall -import io.ktor.application.call -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.pipeline.PipelineContext -import io.ktor.request.httpMethod -import io.ktor.request.path -import io.ktor.response.respondText -import nl.myndocs.oauth2.exception.InvalidGrantException -import nl.myndocs.oauth2.exception.InvalidRequestException -import nl.myndocs.oauth2.exception.OauthException -import nl.myndocs.oauth2.ktor.feature.Oauth2ServerFeature -import nl.myndocs.oauth2.ktor.feature.util.toJson - -suspend fun PipelineContext.configureAuthorizeEndpoint(feature: Oauth2ServerFeature) { - try { - if (call.request.httpMethod != HttpMethod.Get) { - proceed() - return - } - - val requestPath = call.request.path() - if (requestPath != feature.authorizeEndpoint) { - proceed() - return - } - - try { - val allowedResponseTypes = setOf("code", "token") - val responseType = call.request.queryParameters["response_type"] - ?: throw InvalidRequestException("'response_type' not given") - - if (!allowedResponseTypes.contains(responseType)) { - throw InvalidGrantException("'grant_type' with value '$responseType' not allowed") - } - - when (responseType) { - "code" -> configureAuthorizationCodeGranting(feature) - "token" -> configureImplicitTokenGranting(feature) - } - } catch (oauthException: OauthException) { - call.respondText(text = oauthException.toJson(), status = HttpStatusCode.BadRequest) - finish() - return - } - - } catch (t: Throwable) { - t.printStackTrace() - } -} diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/authorize/TokenRouting.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/authorize/TokenRouting.kt deleted file mode 100644 index c4e5075..0000000 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/authorize/TokenRouting.kt +++ /dev/null @@ -1,51 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.routing.authorize - -import io.ktor.application.ApplicationCall -import io.ktor.application.call -import io.ktor.http.HttpStatusCode -import io.ktor.pipeline.PipelineContext -import io.ktor.response.respond -import io.ktor.response.respondRedirect -import nl.myndocs.oauth2.exception.InvalidIdentityException -import nl.myndocs.oauth2.ktor.feature.Oauth2ServerFeature -import nl.myndocs.oauth2.request.RedirectTokenRequest - -suspend fun PipelineContext.configureImplicitTokenGranting(feature: Oauth2ServerFeature) { - val queryParameters = call.request.queryParameters - - val authorizer = feature.authorizerFactory(call) - val credentials = authorizer.extractCredentials() - - try { - val redirect = feature.tokenService.redirect( - RedirectTokenRequest( - queryParameters["client_id"], - queryParameters["redirect_uri"], - credentials?.username ?: "", - credentials?.password ?: "", - queryParameters["scope"] - ), - authorizer.authenticator(), - authorizer.scopesVerifier() - ) - - var stateQueryParameter = "" - - if (queryParameters["state"] != null) { - stateQueryParameter = "&state=" + queryParameters["state"] - } - - call.respondRedirect( - queryParameters["redirect_uri"] + "#access_token=${redirect.accessToken}" + - "&token_type=bearer&expires_in=${redirect.expiresIn()}$stateQueryParameter" - ) - - finish() - return - } catch (unverifiedIdentityException: InvalidIdentityException) { - authorizer.failedAuthentication() - call.respond(HttpStatusCode.Unauthorized) - finish() - return - } -} diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/AuthorizationCodeConsumerRouting.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/AuthorizationCodeConsumerRouting.kt deleted file mode 100644 index 7ea89e7..0000000 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/AuthorizationCodeConsumerRouting.kt +++ /dev/null @@ -1,26 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.routing.token - -import io.ktor.application.ApplicationCall -import io.ktor.application.call -import io.ktor.http.Parameters -import io.ktor.pipeline.PipelineContext -import io.ktor.response.respondText -import nl.myndocs.oauth2.ktor.feature.Oauth2ServerFeature -import nl.myndocs.oauth2.ktor.feature.util.toJson -import nl.myndocs.oauth2.request.AuthorizationCodeRequest - -suspend fun PipelineContext.configureCodeConsumer(feature: Oauth2ServerFeature, formParams: Parameters) { - val accessToken = feature.tokenService.authorize( - AuthorizationCodeRequest( - formParams["client_id"], - formParams["client_secret"], - formParams["code"], - formParams["redirect_uri"] - ) - ) - - call.respondText( - accessToken.toJson(), - io.ktor.http.ContentType.Application.Json - ) -} \ No newline at end of file diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/PasswordGrantRouting.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/PasswordGrantRouting.kt deleted file mode 100644 index 02bcfaf..0000000 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/PasswordGrantRouting.kt +++ /dev/null @@ -1,29 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.routing.token - -import io.ktor.application.ApplicationCall -import io.ktor.application.call -import io.ktor.http.Parameters -import io.ktor.pipeline.PipelineContext -import io.ktor.response.respondText -import nl.myndocs.oauth2.ktor.feature.Oauth2ServerFeature -import nl.myndocs.oauth2.ktor.feature.util.toJson -import nl.myndocs.oauth2.request.PasswordGrantRequest - -suspend fun PipelineContext.configurePasswordGrantRouting(feature: Oauth2ServerFeature, formParams: Parameters) { - val tokenResponse = feature.tokenService.authorize( - PasswordGrantRequest( - formParams["client_id"], - formParams["client_secret"], - formParams["username"], - formParams["password"], - formParams["scope"] - ) - ) - - call.respondText( - tokenResponse.toJson(), - io.ktor.http.ContentType.Application.Json - ) - - finish() -} \ No newline at end of file diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/RefreshTokenRouting.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/RefreshTokenRouting.kt deleted file mode 100644 index 7deec9b..0000000 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/RefreshTokenRouting.kt +++ /dev/null @@ -1,25 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.routing.token - -import io.ktor.application.ApplicationCall -import io.ktor.application.call -import io.ktor.http.Parameters -import io.ktor.pipeline.PipelineContext -import io.ktor.response.respondText -import nl.myndocs.oauth2.ktor.feature.Oauth2ServerFeature -import nl.myndocs.oauth2.ktor.feature.util.toJson -import nl.myndocs.oauth2.request.RefreshTokenRequest - -suspend fun PipelineContext.configureRefreshToken(feature: Oauth2ServerFeature, formParams: Parameters) { - val accessToken = feature.tokenService.refresh( - RefreshTokenRequest( - formParams["client_id"], - formParams["client_secret"], - formParams["refresh_token"] - ) - ) - - call.respondText( - accessToken.toJson(), - io.ktor.http.ContentType.Application.Json - ) -} \ No newline at end of file diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/TokenEndpointRouting.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/TokenEndpointRouting.kt deleted file mode 100644 index 94af30a..0000000 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/routing/token/TokenEndpointRouting.kt +++ /dev/null @@ -1,53 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.routing.token - -import io.ktor.application.ApplicationCall -import io.ktor.application.call -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.pipeline.PipelineContext -import io.ktor.request.httpMethod -import io.ktor.request.path -import io.ktor.request.receiveParameters -import io.ktor.response.respondText -import nl.myndocs.oauth2.exception.InvalidGrantException -import nl.myndocs.oauth2.exception.InvalidRequestException -import nl.myndocs.oauth2.exception.OauthException -import nl.myndocs.oauth2.ktor.feature.Oauth2ServerFeature -import nl.myndocs.oauth2.ktor.feature.util.toJson - -suspend fun PipelineContext.configureTokenEndpoint(feature: Oauth2ServerFeature) { - try { - if (call.request.httpMethod != HttpMethod.Post) { - proceed() - return - } - - if (call.request.path() != feature.tokenEndpoint) { - proceed() - return - } - val formParams = call.receiveParameters() - - try { - val allowedGrantTypes = setOf("password", "authorization_code", "refresh_token") - val grantType = formParams["grant_type"] ?: throw InvalidRequestException("'grant_type' not given") - - if (!allowedGrantTypes.contains(grantType)) { - throw InvalidGrantException("'grant_type' with value '$grantType' not allowed") - } - - when (grantType) { - "password" -> configurePasswordGrantRouting(feature, formParams) - "authorization_code" -> configureCodeConsumer(feature, formParams) - "refresh_token" -> configureRefreshToken(feature, formParams) - } - } catch (oauthException: OauthException) { - call.respondText(text = oauthException.toJson(), status = HttpStatusCode.BadRequest) - finish() - return - } - - } catch (t: Throwable) { - t.printStackTrace() - } -} diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/BasicAuthorizer.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/BasicAuthorizer.kt deleted file mode 100644 index 7dd07d6..0000000 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/BasicAuthorizer.kt +++ /dev/null @@ -1,18 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.util - -import io.ktor.application.ApplicationCall -import io.ktor.request.header -import io.ktor.response.header -import nl.myndocs.oauth2.authenticator.Authorizer -import nl.myndocs.oauth2.authenticator.Credentials - -open class BasicAuthorizer(protected val context: ApplicationCall) : Authorizer { - override fun extractCredentials(): Credentials? { - val authorizationHeader = context.request.header("authorization") ?: "" - return BasicAuth.parseCredentials(authorizationHeader) - } - - override fun failedAuthentication() { - context.response.header("WWW-Authenticate", "Basic realm=\"${context.request.queryParameters["client_id"]}\"") - } -} \ No newline at end of file diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/Credentials.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/Credentials.kt deleted file mode 100644 index 22ccc3f..0000000 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/Credentials.kt +++ /dev/null @@ -1,4 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.util - -@Deprecated("Replaced by core Credentials removed in 0.2.0") -data class Credentials(val username: String?, val password: String?) \ No newline at end of file diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/OauthExceptionUtil.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/OauthExceptionUtil.kt deleted file mode 100644 index a0784c5..0000000 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/OauthExceptionUtil.kt +++ /dev/null @@ -1,7 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.util - -import nl.myndocs.oauth2.exception.OauthException -import nl.myndocs.oauth2.exception.toMap -import nl.myndocs.oauth2.ktor.feature.json.MapToJson - -fun OauthException.toJson() = MapToJson.toJson(this.toMap())!! \ No newline at end of file diff --git a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/TokenResponseUtil.kt b/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/TokenResponseUtil.kt deleted file mode 100644 index 654c33f..0000000 --- a/kotlin-oauth2-server-ktor/src/main/java/nl/myndocs/oauth2/ktor/feature/util/TokenResponseUtil.kt +++ /dev/null @@ -1,7 +0,0 @@ -package nl.myndocs.oauth2.ktor.feature.util - -import nl.myndocs.oauth2.ktor.feature.json.MapToJson -import nl.myndocs.oauth2.response.TokenResponse -import nl.myndocs.oauth2.token.toMap - -fun TokenResponse.toJson() = MapToJson.toJson(this.toMap())!! \ No newline at end of file