11package fi.hsl.jore4.auth.oidc
22
3+ import com.fasterxml.jackson.databind.ObjectMapper
34import com.nimbusds.oauth2.sdk.AuthorizationCode
45import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant
56import com.nimbusds.oauth2.sdk.Scope
@@ -10,9 +11,13 @@ import com.nimbusds.oauth2.sdk.id.ClientID
1011import com.nimbusds.oauth2.sdk.id.State
1112import com.nimbusds.openid.connect.sdk.OIDCTokenResponse
1213import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser
14+ import fi.hsl.jore4.auth.audit.LoginAuditService
1315import jakarta.servlet.http.HttpSession
16+ import okhttp3.OkHttpClient
17+ import okhttp3.Request
1418import org.slf4j.Logger
1519import org.slf4j.LoggerFactory
20+ import org.springframework.beans.factory.annotation.Autowired
1621import org.springframework.beans.factory.annotation.Value
1722import org.springframework.stereotype.Service
1823import java.net.URI
@@ -28,10 +33,17 @@ open class OIDCCodeExchangeService(
2833 private val verificationService : TokenVerificationService ,
2934 @Value(" \$ {loginpage.url}" ) private val loginPageUrl : String
3035) {
36+ @Autowired(required = false )
37+ private var loginAuditService: LoginAuditService ? = null
38+
3139 companion object {
3240 private val LOGGER : Logger = LoggerFactory .getLogger(OIDCCodeExchangeService ::class .java)
41+ private const val SUB_CLAIM = " sub"
42+ private const val NAME_CLAIM = " name"
3343 }
3444
45+ private val httpClient = OkHttpClient .Builder ().build()
46+
3547 /* *
3648 * Exchange the given authorization {@param code} for an access and refresh token.
3749 *
@@ -78,6 +90,7 @@ open class OIDCCodeExchangeService(
7890 // get the access token and refresh token
7991 val accessToken = successResponse.oidcTokens.accessToken
8092 val refreshToken = successResponse.oidcTokens.refreshToken
93+ val idToken = successResponse.oidcTokens.idToken
8194
8295 // verify token authenticity and validity if not using Entra, as it uses an unverifiable internal token
8396 // See https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens#validate-tokens
@@ -87,11 +100,74 @@ open class OIDCCodeExchangeService(
87100
88101 session.setAttribute(SessionKeys .USER_TOKEN_SET_KEY , UserTokenSet (accessToken, refreshToken))
89102
103+ // Record the login event in the audit log
104+ try {
105+ loginAuditService?.let {
106+ val userId = idToken.jwtClaimsSet.subject
107+ val userName = fetchUserNameFromUserInfo(accessToken.value, userId)
108+ it.recordLogin(userId, userName)
109+ }
110+ } catch (e: Exception ) {
111+ LOGGER .warn(" Could not record login audit" , e)
112+ }
113+
90114 // redirect the user to the login page URL
91115 val redirectUri = URI .create(loginPageUrl)
92116
93117 LOGGER .debug(" Created redirect URI: {}" , redirectUri)
94118
95119 return redirectUri
96120 }
121+
122+ /* *
123+ * Fetch the user's name from the OIDC UserInfo endpoint.
124+ */
125+ private fun fetchUserNameFromUserInfo (
126+ accessTokenValue : String ,
127+ expectedSub : String
128+ ): String? =
129+ runCatching {
130+ val request =
131+ Request
132+ .Builder ()
133+ .addHeader(" Authorization" , " Bearer $accessTokenValue " )
134+ .addHeader(" Accept" , " application/json" )
135+ .get()
136+ .url(oidcProviderMetadataSupplier.providerMetadata.userInfoEndpointURI.toURL())
137+ .build()
138+
139+ httpClient
140+ .newCall(request)
141+ .execute()
142+ .use { response ->
143+ if (! response.isSuccessful) {
144+ LOGGER .warn(" Failed to fetch UserInfo, status: {}" , response.code)
145+ return null
146+ }
147+
148+ val responseBody =
149+ response.body?.string() ? : run {
150+ LOGGER .warn(" UserInfo response body is null" )
151+ return null
152+ }
153+
154+ val result = ObjectMapper ().readValue(responseBody, Map ::class .java) as Map <* , * >
155+
156+ // Verify that the sub claim matches the ID token's sub
157+ val userInfoSub = result[SUB_CLAIM ]?.toString()
158+ if (userInfoSub != expectedSub) {
159+ LOGGER .error(
160+ " UserInfo sub claim '{}' does not match ID token sub '{}'." ,
161+ userInfoSub,
162+ expectedSub
163+ )
164+ return null
165+ }
166+
167+ result[NAME_CLAIM ]?.toString()
168+ }
169+ }.getOrElse { e ->
170+ LOGGER .warn(" Error fetching user name from UserInfo endpoint" , e)
171+ null
172+ }
97173}
0 commit comments