Skip to content

Commit 099839d

Browse files
committed
Userinfo endpoint
Fix issue with scope fallback not working correctly Stream line ktor error messaging Support Javalin framework
1 parent 64eed1e commit 099839d

File tree

16 files changed

+426
-53
lines changed

16 files changed

+426
-53
lines changed

kotlin-oauth2-server-core/src/main/java/nl/myndocs/oauth2/TokenService.kt

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package nl.myndocs.oauth2
33
import nl.myndocs.oauth2.client.ClientService
44
import nl.myndocs.oauth2.exception.*
55
import nl.myndocs.oauth2.identity.IdentityService
6+
import nl.myndocs.oauth2.identity.UserInfo
67
import nl.myndocs.oauth2.request.*
78
import nl.myndocs.oauth2.response.TokenResponse
89
import nl.myndocs.oauth2.scope.ScopeParser
@@ -168,13 +169,21 @@ class TokenService(
168169
throw InvalidIdentityException()
169170
}
170171

171-
val requestedScopes = ScopeParser.parseScopes(redirect.scope)
172+
var requestedScopes = ScopeParser.parseScopes(redirect.scope)
173+
174+
if (redirect.scope == null) {
175+
requestedScopes = clientOf.clientScopes
176+
}
172177

173178
val scopesAllowed = scopesAllowed(clientOf.clientScopes, requestedScopes)
174179
if (!scopesAllowed) {
175180
throw InvalidScopeException(requestedScopes.minus(clientOf.clientScopes))
176181
}
177182

183+
if (!identityService.validScopes(clientOf, identityOf, requestedScopes)) {
184+
throw InvalidScopeException(requestedScopes)
185+
}
186+
178187
val codeToken = codeTokenConverter.convertToToken(
179188
identityOf.username,
180189
clientOf.clientId,
@@ -217,13 +226,21 @@ class TokenService(
217226
throw InvalidIdentityException()
218227
}
219228

220-
val requestedScopes = ScopeParser.parseScopes(redirect.scope)
229+
var requestedScopes = ScopeParser.parseScopes(redirect.scope)
230+
231+
if (redirect.scope == null) {
232+
requestedScopes = clientOf.clientScopes
233+
}
221234

222235
val scopesAllowed = scopesAllowed(clientOf.clientScopes, requestedScopes)
223236
if (!scopesAllowed) {
224237
throw InvalidScopeException(requestedScopes.minus(clientOf.clientScopes))
225238
}
226239

240+
if (!identityService.validScopes(clientOf, identityOf, requestedScopes)) {
241+
throw InvalidScopeException(requestedScopes)
242+
}
243+
227244
val accessToken = accessTokenConverter.convertToToken(
228245
identityOf.username,
229246
clientOf.clientId,
@@ -236,6 +253,18 @@ class TokenService(
236253
return accessToken
237254
}
238255

256+
fun userInfo(accessToken: String): UserInfo {
257+
val storedAccessToken = tokenStore.accessToken(accessToken)!!
258+
val client = clientService.clientOf(storedAccessToken.clientId)!!
259+
val identity = identityService.identityOf(client, storedAccessToken.username)!!
260+
261+
return UserInfo(
262+
identity,
263+
client,
264+
storedAccessToken.scopes
265+
)
266+
}
267+
239268
private fun throwExceptionIfUnverifiedClient(clientRequest: ClientRequest) {
240269
if (clientRequest.clientId == null) {
241270
throw InvalidRequestException(INVALID_REQUEST_FIELD_MESSAGE.format("client_id"))
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package nl.myndocs.oauth2.exception
2+
3+
fun OauthException.toMap(): Map<String, String> {
4+
5+
val mutableMapOf = mutableMapOf<String, String>(
6+
"error" to this.error.errorName
7+
)
8+
9+
if (this.errorDescription != null) {
10+
mutableMapOf["error_description"] = this.errorDescription
11+
}
12+
13+
return mutableMapOf.toMap()
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package nl.myndocs.oauth2.identity
2+
3+
import nl.myndocs.oauth2.client.Client
4+
5+
data class UserInfo(
6+
val identity: Identity,
7+
val client: Client,
8+
val scopes: Set<String>
9+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package nl.myndocs.oauth2.token
2+
3+
import nl.myndocs.oauth2.response.TokenResponse
4+
5+
fun TokenResponse.toMap() = mapOf(
6+
"access_token" to this.accessToken,
7+
"token_type" to this.tokenType,
8+
"expires_in" to this.expiresIn,
9+
"refresh_token" to this.refreshToken
10+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<artifactId>kotlin-oauth2-server</artifactId>
7+
<groupId>nl.myndocs</groupId>
8+
<version>0.1.0</version>
9+
</parent>
10+
<modelVersion>4.0.0</modelVersion>
11+
12+
<artifactId>kotlin-oauth2-server-javalin</artifactId>
13+
14+
15+
<dependencies>
16+
<dependency>
17+
<groupId>nl.myndocs</groupId>
18+
<artifactId>kotlin-oauth2-server-core</artifactId>
19+
<version>${project.version}</version>
20+
<scope>provided</scope>
21+
</dependency>
22+
<dependency>
23+
<groupId>io.javalin</groupId>
24+
<artifactId>javalin</artifactId>
25+
<version>2.0.0</version>
26+
<scope>provided</scope>
27+
</dependency>
28+
</dependencies>
29+
</project>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package nl.myndocs.oauth2.javalin
2+
3+
import io.javalin.Javalin
4+
import io.javalin.apibuilder.ApiBuilder.*
5+
import nl.myndocs.oauth2.TokenService
6+
import nl.myndocs.oauth2.client.ClientService
7+
import nl.myndocs.oauth2.exception.InvalidGrantException
8+
import nl.myndocs.oauth2.exception.InvalidRequestException
9+
import nl.myndocs.oauth2.exception.OauthException
10+
import nl.myndocs.oauth2.exception.toMap
11+
import nl.myndocs.oauth2.identity.IdentityService
12+
import nl.myndocs.oauth2.identity.UserInfo
13+
import nl.myndocs.oauth2.javalin.routing.*
14+
import nl.myndocs.oauth2.token.TokenStore
15+
import nl.myndocs.oauth2.token.converter.*
16+
17+
data class OauthConfiguration(
18+
var identityService: IdentityService? = null,
19+
var clientService: ClientService? = null,
20+
var tokenStore: TokenStore? = null,
21+
var tokenEndpoint: String = "/oauth/token",
22+
var authorizeEndpoint: String = "/oauth/authorize",
23+
var userInfoEndpoint: String = "/oauth/userinfo",
24+
var accessTokenConverter: AccessTokenConverter = UUIDAccessTokenConverter(),
25+
var refreshTokenConverter: RefreshTokenConverter = UUIDRefreshTokenConverter(),
26+
var codeTokenConverter: CodeTokenConverter = UUIDCodeTokenConverter(),
27+
var userInfoCallback: (UserInfo) -> Map<String, Any?> = { userInfo ->
28+
mapOf(
29+
"username" to userInfo.identity.username,
30+
"scopes" to userInfo.scopes
31+
)
32+
}
33+
)
34+
35+
fun Javalin.enableOauthServer(configurationCallback: OauthConfiguration.() -> Unit) {
36+
val configuration = OauthConfiguration()
37+
configuration.configurationCallback()
38+
39+
val tokenService = TokenService(
40+
configuration.identityService!!,
41+
configuration.clientService!!,
42+
configuration.tokenStore!!,
43+
configuration.accessTokenConverter,
44+
configuration.refreshTokenConverter,
45+
configuration.codeTokenConverter
46+
)
47+
48+
this.routes {
49+
path(configuration.tokenEndpoint) {
50+
post { ctx ->
51+
try {
52+
val allowedGrantTypes = setOf("password", "authorization_code", "refresh_token")
53+
val grantType = ctx.formParam("grant_type")
54+
?: throw InvalidRequestException("'grant_type' not given")
55+
56+
if (!allowedGrantTypes.contains(grantType)) {
57+
throw InvalidGrantException("'grant_type' with value '$grantType' not allowed")
58+
}
59+
60+
val paramMap = ctx.formParamMap()
61+
.mapValues { ctx.formParam(it.key) }
62+
63+
when (grantType) {
64+
"password" -> routePasswordGrant(ctx, tokenService, paramMap)
65+
"authorization_code" -> routeAuthorizationCodeGrant(ctx, tokenService, paramMap)
66+
"refresh_token" -> routeRefreshTokenGrant(ctx, tokenService, paramMap)
67+
}
68+
} catch (oauthException: OauthException) {
69+
ctx.status(400)
70+
ctx.json(oauthException.toMap())
71+
}
72+
73+
}
74+
}
75+
76+
path(configuration.authorizeEndpoint) {
77+
get { ctx ->
78+
try {
79+
val allowedResponseTypes = setOf("code", "token")
80+
val responseType = ctx.queryParam("response_type")
81+
?: throw InvalidRequestException("'response_type' not given")
82+
83+
if (!allowedResponseTypes.contains(responseType)) {
84+
throw InvalidGrantException("'grant_type' with value '$responseType' not allowed")
85+
}
86+
87+
val paramMap = ctx.queryParamMap()
88+
.mapValues { ctx.queryParam(it.key) }
89+
90+
when (responseType) {
91+
"code" -> routeAuthorizationCodeRedirect(ctx, tokenService, paramMap)
92+
"token" -> routeAccessTokenRedirect(ctx, tokenService, paramMap)
93+
}
94+
} catch (oauthException: OauthException) {
95+
ctx.status(400)
96+
ctx.json(oauthException.toMap())
97+
}
98+
}
99+
}
100+
101+
path(configuration.userInfoEndpoint) {
102+
get { ctx ->
103+
val authorization = ctx.header("Authorization")
104+
105+
if (authorization == null) {
106+
ctx.status(401)
107+
return@get
108+
}
109+
110+
if (!authorization.startsWith("bearer ", true)) {
111+
ctx.status(401)
112+
return@get
113+
}
114+
115+
val token = authorization.substring(7)
116+
117+
val userInfoCallback = configuration.userInfoCallback(tokenService.userInfo(token))
118+
119+
ctx.json(userInfoCallback)
120+
}
121+
}
122+
}
123+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package nl.myndocs.oauth2.javalin.routing
2+
3+
import io.javalin.Context
4+
import nl.myndocs.oauth2.TokenService
5+
import nl.myndocs.oauth2.exception.InvalidIdentityException
6+
import nl.myndocs.oauth2.ktor.feature.util.BasicAuth
7+
import nl.myndocs.oauth2.request.RedirectAuthorizationCodeRequest
8+
import nl.myndocs.oauth2.request.RedirectTokenRequest
9+
10+
11+
fun routeAuthorizationCodeRedirect(ctx: Context, tokenService: TokenService, queryParameters: Map<String, String?>) {
12+
val authorizationHeader = ctx.header("authorization") ?: ""
13+
val credentials = BasicAuth.parse(authorizationHeader)
14+
15+
try {
16+
val redirect = tokenService.redirect(
17+
RedirectAuthorizationCodeRequest(
18+
queryParameters["client_id"],
19+
queryParameters["redirect_uri"],
20+
credentials.username ?: "",
21+
credentials.password ?: "",
22+
queryParameters["scope"]
23+
)
24+
)
25+
26+
27+
ctx.redirect(queryParameters["redirect_uri"] + "?code=${redirect.codeToken}")
28+
} catch (unverifiedIdentityException: InvalidIdentityException) {
29+
ctx.header("WWW-Authenticate", "Basic realm=\"${queryParameters["client_id"]}\"")
30+
ctx.status(401)
31+
}
32+
}
33+
34+
35+
fun routeAccessTokenRedirect(ctx: Context, tokenService: TokenService, queryParameters: Map<String, String?>) {
36+
val authorizationHeader = ctx.header("authorization") ?: ""
37+
val credentials = BasicAuth.parse(authorizationHeader)
38+
39+
try {
40+
val redirect = tokenService.redirect(
41+
RedirectTokenRequest(
42+
queryParameters["client_id"],
43+
queryParameters["redirect_uri"],
44+
credentials.username ?: "",
45+
credentials.password ?: "",
46+
queryParameters["scope"]
47+
)
48+
)
49+
50+
ctx.redirect(
51+
queryParameters["redirect_uri"] + "#access_token=${redirect.accessToken}" +
52+
"&token_type=bearer&expires_in=${redirect.expiresIn()}"
53+
)
54+
55+
} catch (unverifiedIdentityException: InvalidIdentityException) {
56+
ctx.header("WWW-Authenticate", "Basic realm=\"${queryParameters["client_id"]}\"")
57+
ctx.status(401)
58+
}
59+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package nl.myndocs.oauth2.javalin.routing
2+
3+
import io.javalin.Context
4+
import nl.myndocs.oauth2.TokenService
5+
import nl.myndocs.oauth2.request.AuthorizationCodeRequest
6+
import nl.myndocs.oauth2.request.PasswordGrantRequest
7+
import nl.myndocs.oauth2.request.RefreshTokenRequest
8+
import nl.myndocs.oauth2.token.toMap
9+
10+
11+
fun routePasswordGrant(ctx: Context, tokenService: TokenService, formParams: Map<String, String?>) {
12+
val tokenResponse = tokenService.authorize(
13+
PasswordGrantRequest(
14+
formParams["client_id"],
15+
formParams["client_secret"],
16+
formParams["username"],
17+
formParams["password"],
18+
formParams["scope"]
19+
)
20+
)
21+
22+
ctx.json(tokenResponse.toMap())
23+
}
24+
25+
fun routeRefreshTokenGrant(ctx: Context, tokenService: TokenService, formParams: Map<String, String?>) {
26+
val accessToken = tokenService.refresh(
27+
RefreshTokenRequest(
28+
formParams["client_id"],
29+
formParams["client_secret"],
30+
formParams["refresh_token"]
31+
)
32+
)
33+
34+
ctx.json(accessToken.toMap())
35+
}
36+
37+
fun routeAuthorizationCodeGrant(ctx: Context, tokenService: TokenService, formParams: Map<String, String?>) {
38+
val accessToken = tokenService.authorize(
39+
AuthorizationCodeRequest(
40+
formParams["client_id"],
41+
formParams["client_secret"],
42+
formParams["code"],
43+
formParams["redirect_uri"]
44+
)
45+
)
46+
47+
ctx.json(accessToken.toMap())
48+
}

0 commit comments

Comments
 (0)