diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala index 61dd05a..c29d059 100644 --- a/src/main/scala/gitbucket/core/controller/IndexController.scala +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -1,7 +1,8 @@ package gitbucket.core.controller -import java.net.URI +import com.nimbusds.jwt.JWT +import java.net.URI import com.nimbusds.oauth2.sdk.id.State import com.nimbusds.openid.connect.sdk.Nonce import gitbucket.core.helper.xml @@ -57,7 +58,8 @@ // // case class SearchForm(query: String, owner: String, repository: String) - case class OidcContext(state: State, nonce: Nonce, redirectBackURI: String) + case class OidcAuthContext(state: State, nonce: Nonce, redirectBackURI: String) + case class OidcSessionContext(token: JWT) get("/") { context.loginAccount @@ -120,8 +122,8 @@ case _ => "/" } session.setAttribute( - Keys.Session.OidcContext, - OidcContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI) + Keys.Session.OidcAuthContext, + OidcAuthContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI) ) redirect(authenticationRequest.toURI.toString) } getOrElse { @@ -135,10 +137,12 @@ get("/signin/oidc") { context.settings.oidc.map { oidc => val redirectURI = new URI(s"$baseUrl/signin/oidc") - session.get(Keys.Session.OidcContext) match { - case Some(context: OidcContext) => - authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map { account => - signin(account, context.redirectBackURI) + session.get(Keys.Session.OidcAuthContext) match { + case Some(context: OidcAuthContext) => + authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map { + case (jwt, account) => + session.setAttribute(Keys.Session.OidcSessionContext, OidcSessionContext(jwt)) + signin(account, context.redirectBackURI) } orElse { flash.update("error", "Sorry, authentication failed. Please try again.") session.invalidate() @@ -155,6 +159,15 @@ } get("/signout") { + context.settings.oidc.map { oidc => + session.get(Keys.Session.OidcSessionContext).foreach { + case context: OidcSessionContext => + val redirectURI = new URI(baseUrl) + val authenticationRequest = createOIDLogoutRequest(oidc.issuer, oidc.clientID, redirectURI, context.token) + session.invalidate + redirect(authenticationRequest.toURI.toString) + } + } session.invalidate if (isDevFeatureEnabled(DevFeatures.KeepSession)) { deleteLoginAccountFromLocalFile() diff --git a/src/main/scala/gitbucket/core/service/OpenIDConnectService.scala b/src/main/scala/gitbucket/core/service/OpenIDConnectService.scala index 063561f..2b102a2 100644 --- a/src/main/scala/gitbucket/core/service/OpenIDConnectService.scala +++ b/src/main/scala/gitbucket/core/service/OpenIDConnectService.scala @@ -1,11 +1,11 @@ package gitbucket.core.service import java.net.URI - import com.nimbusds.jose.JWSAlgorithm.Family import com.nimbusds.jose.proc.BadJOSEException import com.nimbusds.jose.util.DefaultResourceRetriever import com.nimbusds.jose.{JOSEException, JWSAlgorithm} +import com.nimbusds.jwt.JWT import com.nimbusds.oauth2.sdk._ import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer, State} @@ -52,6 +52,11 @@ ) } + def createOIDLogoutRequest(issuer: Issuer, clientID: ClientID, redirectURI: URI, token: JWT): LogoutRequest = { + val metadata = OIDCProviderMetadata.resolve(issuer) + new LogoutRequest(metadata.getEndSessionEndpointURI, token, null, clientID, redirectURI, null, null) + } + /** * Proceed the OpenID Connect authentication. * @@ -60,7 +65,7 @@ * @param state State saved in the session * @param nonce Nonce saved in the session * @param oidc OIDC settings - * @return ID token + * @return (ID token, GitBucket account) */ def authenticate( params: Map[String, String], @@ -68,22 +73,25 @@ state: State, nonce: Nonce, oidc: SystemSettingsService.OIDC - )(implicit s: Session): Option[Account] = + )(implicit s: Session): Option[(JWT, Account)] = validateOIDCAuthenticationResponse(params, state, redirectURI) flatMap { authenticationResponse => - obtainOIDCToken(authenticationResponse.getAuthorizationCode, nonce, redirectURI, oidc) flatMap { claims => - Seq("email", "preferred_username", "name").map(k => Option(claims.getStringClaim(k))) match { - case Seq(Some(email), preferredUsername, name) => - getOrCreateFederatedUser( - claims.getIssuer.getValue, - claims.getSubject.getValue, - email, - preferredUsername, - name - ) - case _ => - logger.info(s"OIDC ID token must have an email claim: claims=${claims.toJSONObject}") - None - } + obtainOIDCToken(authenticationResponse.getAuthorizationCode, nonce, redirectURI, oidc) flatMap { + case (jwt, claims) => + Seq("email", "preferred_username", "name").map(k => Option(claims.getStringClaim(k))) match { + case Seq(Some(email), preferredUsername, name) => + getOrCreateFederatedUser( + claims.getIssuer.getValue, + claims.getSubject.getValue, + email, + preferredUsername, + name + ).map { account => + (jwt, account) + } + case _ => + logger.info(s"OIDC ID token must have an email claim: claims=${claims.toJSONObject}") + None + } } } @@ -136,7 +144,7 @@ nonce: Nonce, redirectURI: URI, oidc: SystemSettingsService.OIDC - ): Option[IDTokenClaimsSet] = { + ): Option[(JWT, IDTokenClaimsSet)] = { val metadata = OIDCProviderMetadata.resolve(oidc.issuer) val tokenRequest = new TokenRequest( metadata.getTokenEndpointURI, @@ -173,7 +181,7 @@ metadata: OIDCProviderMetadata, nonce: Nonce, oidc: SystemSettingsService.OIDC - ): Option[IDTokenClaimsSet] = + ): Option[(JWT, IDTokenClaimsSet)] = Option(response.getOIDCTokens.getIDToken) match { case Some(jwt) => val validator = oidc.jwsAlgorithm map { jwsAlgorithm => @@ -188,7 +196,7 @@ new IDTokenValidator(metadata.getIssuer, oidc.clientID) } try { - Some(validator.validate(jwt, nonce)) + Some((jwt, validator.validate(jwt, nonce))) } catch { case e @ (_: BadJOSEException | _: JOSEException) => logger.info(s"OIDC ID token has error: $e") diff --git a/src/main/scala/gitbucket/core/util/Keys.scala b/src/main/scala/gitbucket/core/util/Keys.scala index cf24913..c7b4311 100644 --- a/src/main/scala/gitbucket/core/util/Keys.scala +++ b/src/main/scala/gitbucket/core/util/Keys.scala @@ -28,7 +28,12 @@ /** * Session key for the OpenID Connect authentication. */ - val OidcContext = "oidcContext" + val OidcAuthContext = "oidcAuthContext" + + /** + * Session key for the OpenID Connect token. + */ + val OidcSessionContext = "oidcSessionContext" /** * Generate session key for the issue search condition.