Skip to content

Commit 61e09e1

Browse files
authored
Merge pull request #865 from Lutonite/fix/introspect-date
fix(introspect): exp, iat, nbf claims were always null
2 parents d746f02 + f19c2ac commit 61e09e1

File tree

2 files changed

+91
-14
lines changed

2 files changed

+91
-14
lines changed

src/main/kotlin/no/nav/security/mock/oauth2/introspect/Introspect.kt

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package no.nav.security.mock.oauth2.introspect
22

3+
import com.fasterxml.jackson.annotation.JsonFormat
34
import com.fasterxml.jackson.annotation.JsonInclude
45
import com.fasterxml.jackson.annotation.JsonProperty
56
import com.nimbusds.jwt.JWTClaimsSet
7+
import com.nimbusds.jwt.util.DateUtils
68
import com.nimbusds.oauth2.sdk.OAuth2Error
79
import mu.KotlinLogging
810
import no.nav.security.mock.oauth2.OAuth2Exception
@@ -13,6 +15,7 @@ import no.nav.security.mock.oauth2.http.Route
1315
import no.nav.security.mock.oauth2.http.json
1416
import no.nav.security.mock.oauth2.token.OAuth2TokenProvider
1517
import okhttp3.Headers
18+
import java.util.Date
1619

1720
private val log = KotlinLogging.logger { }
1821

@@ -26,21 +29,20 @@ internal fun Route.Builder.introspect(tokenProvider: OAuth2TokenProvider) =
2629
}
2730

2831
request.verifyToken(tokenProvider)?.let {
29-
val claims = it.claims
3032
json(
3133
IntrospectResponse(
32-
true,
33-
claims["scope"].toString(),
34-
claims["client_id"].toString(),
35-
claims["username"].toString(),
36-
claims["token_type"].toString(),
37-
claims["exp"] as? Long,
38-
claims["iat"] as? Long,
39-
claims["nbf"] as? Long,
40-
claims["sub"].toString(),
41-
claims["aud"].toString(),
42-
claims["iss"].toString(),
43-
claims["jti"].toString(),
34+
active = true,
35+
scope = it.getStringClaim("scope"),
36+
clientId = it.getStringClaim("client_id"),
37+
username = it.getStringClaim("username"),
38+
tokenType = it.getStringClaim("token_type") ?: "Bearer",
39+
exp = it.expirationTime.epochSeconds(),
40+
iat = it.issueTime.epochSeconds(),
41+
nbf = it.notBeforeTime.epochSeconds(),
42+
sub = it.subject,
43+
aud = it.audience,
44+
iss = it.issuer,
45+
jti = it.jwtid,
4446
),
4547
)
4648
} ?: json(IntrospectResponse(false))
@@ -70,6 +72,8 @@ private fun String.auth(method: String): String? =
7072
.takeIf { it.size == 2 }
7173
?.last()
7274

75+
private fun Date?.epochSeconds(): Long? = this?.let(DateUtils::toSecondsSinceEpoch)
76+
7377
@JsonInclude(JsonInclude.Include.NON_NULL)
7478
data class IntrospectResponse(
7579
@JsonProperty("active")
@@ -91,7 +95,8 @@ data class IntrospectResponse(
9195
@JsonProperty("sub")
9296
val sub: String? = null,
9397
@JsonProperty("aud")
94-
val aud: String? = null,
98+
@JsonFormat(with = [JsonFormat.Feature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED])
99+
val aud: List<String>? = null,
95100
@JsonProperty("iss")
96101
val iss: String? = null,
97102
@JsonProperty("jti")

src/test/kotlin/no/nav/security/mock/oauth2/introspect/IntrospectTest.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,78 @@ internal class IntrospectTest {
9191
}
9292
}
9393

94+
@Test
95+
fun `introspect should return iat and exp from claims when present in token`() {
96+
val issuerUrl = "http://localhost/default"
97+
val tokenProvider = OAuth2TokenProvider()
98+
val claims =
99+
mapOf(
100+
"iss" to issuerUrl,
101+
"client_id" to "yolo",
102+
"token_type" to "token",
103+
"sub" to "foo",
104+
"iat" to Instant.now().epochSecond,
105+
"exp" to Instant.now().plus(1, ChronoUnit.DAYS).epochSecond,
106+
)
107+
108+
val token = tokenProvider.jwt(claims)
109+
val request = request("$issuerUrl$INTROSPECT", token.serialize())
110+
111+
routes { introspect(tokenProvider) }.invoke(request).asClue {
112+
it.status shouldBe 200
113+
val response = it.parse<IntrospectResponse>()
114+
response.active shouldBe true
115+
response.iat shouldBe claims["iat"]
116+
response.exp shouldBe claims["exp"]
117+
}
118+
}
119+
120+
@Test
121+
fun `introspect should return single audience as string`() {
122+
val issuerUrl = "http://localhost/default"
123+
val tokenProvider = OAuth2TokenProvider()
124+
val claims =
125+
mapOf(
126+
"iss" to issuerUrl,
127+
"client_id" to "yolo",
128+
"token_type" to "token",
129+
"sub" to "foo",
130+
"aud" to "some-audience",
131+
)
132+
val token = tokenProvider.jwt(claims)
133+
val request = request("$issuerUrl$INTROSPECT", token.serialize())
134+
135+
routes { introspect(tokenProvider) }.invoke(request).asClue {
136+
it.status shouldBe 200
137+
val response = it.parse<Map<String, Any>>()
138+
response shouldContainAll claims
139+
response shouldContain ("active" to true)
140+
}
141+
}
142+
143+
@Test
144+
fun `introspect should return multiple audiences as array of strings`() {
145+
val issuerUrl = "http://localhost/default"
146+
val tokenProvider = OAuth2TokenProvider()
147+
val claims =
148+
mapOf(
149+
"iss" to issuerUrl,
150+
"client_id" to "yolo",
151+
"token_type" to "token",
152+
"sub" to "foo",
153+
"aud" to listOf("audience1", "audience2"),
154+
)
155+
val token = tokenProvider.jwt(claims)
156+
val request = request("$issuerUrl$INTROSPECT", token.serialize())
157+
158+
routes { introspect(tokenProvider) }.invoke(request).asClue {
159+
it.status shouldBe 200
160+
val response = it.parse<Map<String, Any>>()
161+
response shouldContainAll claims
162+
response shouldContain ("active" to true)
163+
}
164+
}
165+
94166
@Test
95167
fun `introspect should return active false when token is missing`() {
96168
val url = "http://localhost/default$INTROSPECT"

0 commit comments

Comments
 (0)