Add OpenID Connect authentication feature

This commit is contained in:
Hidetake Iwata
2018-01-05 00:49:57 +09:00
parent fb34b0909e
commit ab10b77c50
14 changed files with 526 additions and 47 deletions

View File

@@ -1,4 +1,4 @@
import com.typesafe.sbt.license.{LicenseInfo, DepModuleInfo}
import com.typesafe.sbt.license.{DepModuleInfo, LicenseInfo}
import com.typesafe.sbt.pgp.PgpKeys._
val Organization = "io.github.gitbucket"
@@ -55,6 +55,7 @@ libraryDependencies ++= Seq(
"com.enragedginger" %% "akka-quartz-scheduler" % "1.6.1-akka-2.5.x" exclude("c3p0","c3p0"),
"net.coobird" % "thumbnailator" % "0.4.8",
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.nimbusds" % "oauth2-oidc-sdk" % "5.45",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.12" % "test",
@@ -127,8 +128,8 @@ libraryDependencies ++= Seq(
val executableKey = TaskKey[File]("executable")
executableKey := {
import java.util.jar.{ Manifest => JarManifest }
import java.util.jar.Attributes.{ Name => AttrName }
import java.util.jar.Attributes.{Name => AttrName}
import java.util.jar.{Manifest => JarManifest}
val workDir = Keys.target.value / "executable"
val warName = Keys.name.value + ".war"

View File

@@ -28,4 +28,12 @@
</createTable>
<addPrimaryKey constraintName="IDX_RELEASE_ASSET_PK" tableName="RELEASE_ASSET" columnNames="USER_NAME, REPOSITORY_NAME, TAG, FILE_NAME"/>
<addForeignKeyConstraint constraintName="IDX_RELEASE_ASSET_FK1" baseTableName="RELEASE_ASSET" baseColumnNames="USER_NAME, REPOSITORY_NAME, TAG" referencedTableName="RELEASE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, TAG"/>
<createTable tableName="ACCOUNT_FEDERATION">
<column name="ISSUER" type="varchar(100)" nullable="false"/>
<column name="SUBJECT" type="varchar(100)" nullable="false"/>
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_ACCOUNT_FEDERATION_PK" tableName="ACCOUNT_FEDERATION" columnNames="ISSUER, SUBJECT"/>
<addForeignKeyConstraint constraintName="IDX_ACCOUNT_FEDERATION_FK0" baseTableName="ACCOUNT_FEDERATION" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
</changeSet>

View File

@@ -1,23 +1,38 @@
package gitbucket.core.controller
import java.net.URI
import com.nimbusds.oauth2.sdk.id.State
import com.nimbusds.openid.connect.sdk.Nonce
import gitbucket.core.helper.xml
import gitbucket.core.model.Account
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, UsersAuthenticator}
import org.scalatra.forms._
import org.scalatra.Ok
import org.scalatra.forms._
class IndexController extends IndexControllerBase
with RepositoryService with ActivityService with AccountService with RepositorySearchService with IssuesService
with UsersAuthenticator with ReferrerAuthenticator
with RepositoryService
with ActivityService
with AccountService
with RepositorySearchService
with IssuesService
with UsersAuthenticator
with ReferrerAuthenticator
with OpenIDConnectService
trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with RepositorySearchService
with UsersAuthenticator with ReferrerAuthenticator =>
self: RepositoryService
with ActivityService
with AccountService
with RepositorySearchService
with UsersAuthenticator
with ReferrerAuthenticator
with OpenIDConnectService =>
case class SignInForm(userName: String, password: String, hash: Option[String])
@@ -55,14 +70,62 @@ trait IndexControllerBase extends ControllerBase {
post("/signin", signinForm){ form =>
authenticate(context.settings, form.userName, form.password) match {
case Some(account) => signin(account, form.hash)
case None => {
case Some(account) =>
flash.get(Keys.Flash.Redirect) match {
case Some(redirectUrl: String) => signin(account, redirectUrl + form.hash.getOrElse(""))
case _ => signin(account)
}
case None =>
flash += "userName" -> form.userName
flash += "password" -> form.password
flash += "error" -> "Sorry, your Username and/or Password is incorrect. Please try again."
redirect("/signin")
}
}
/**
* Initiate an OpenID Connect authentication request.
*/
post("/signin/oidc") {
context.settings.oidc.map { oidc =>
val redirectURI = new URI(s"$baseUrl/signin/oidc")
val authenticationRequest = createOIDCAuthenticationRequest(oidc.issuer, oidc.clientID, redirectURI)
session.setAttribute(Keys.Session.OidcState, authenticationRequest.getState)
session.setAttribute(Keys.Session.OidcNonce, authenticationRequest.getNonce)
session.setAttribute(Keys.Session.OidcRedirectBackURI,
flash.get(Keys.Flash.Redirect) match {
case Some(redirectBackURI: String) => redirectBackURI + params.getOrElse("hash", "")
case _ => "/"
})
redirect(authenticationRequest.toURI.toString)
} getOrElse {
NotFound()
}
}
/**
* Handle an OpenID Connect authentication response.
*/
get("/signin/oidc") {
context.settings.oidc.map { oidc =>
val redirectURI = new URI(s"$baseUrl/signin/oidc")
Seq(Keys.Session.OidcState, Keys.Session.OidcNonce, Keys.Session.OidcRedirectBackURI).map(session.get(_)) match {
case Seq(Some(state: State), Some(nonce: Nonce), Some(redirectBackURI: String)) =>
authenticate(params, redirectURI, state, nonce, oidc) map { account =>
signin(account, redirectBackURI)
} orElse {
flash += "error" -> "Sorry, authentication failed. Please try again."
session.invalidate()
redirect("/signin")
}
case _ =>
flash += "error" -> "Sorry, something wrong. Please try again."
session.invalidate()
redirect("/signin")
}
} getOrElse {
NotFound()
}
}
get("/signout"){
@@ -87,7 +150,7 @@ trait IndexControllerBase extends ControllerBase {
/**
* Set account information into HttpSession and redirect.
*/
private def signin(account: Account, hash: Option[String]) = {
private def signin(account: Account, redirectUrl: String = "/") = {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
@@ -95,14 +158,10 @@ trait IndexControllerBase extends ControllerBase {
redirect("/" + account.userName + "/_edit")
}
flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
if(redirectUrl.stripSuffix("/") == request.getContextPath){
if (redirectUrl.stripSuffix("/") == request.getContextPath) {
redirect("/")
} else {
redirect(redirectUrl + hash.getOrElse(""))
}
}.getOrElse {
redirect("/")
redirect(redirectUrl)
}
}

View File

@@ -2,23 +2,23 @@ package gitbucket.core.controller
import java.io.FileInputStream
import gitbucket.core.admin.html
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
import gitbucket.core.util.{AdminAuthenticator, Mailer}
import gitbucket.core.ssh.SshServer
import gitbucket.core.plugin.{PluginInfoBase, PluginRegistry, PluginRepository}
import SystemSettingsService._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
import gitbucket.core.util.StringUtil._
import org.scalatra.forms._
import org.apache.commons.io.IOUtils
import org.scalatra.i18n.Messages
import com.github.zafarkhaja.semver.{Version => Semver}
import gitbucket.core.GitBucketCoreModule
import org.scalatra._
import gitbucket.core.admin.html
import gitbucket.core.plugin.{PluginInfoBase, PluginRegistry, PluginRepository}
import gitbucket.core.service.SystemSettingsService._
import gitbucket.core.service.{AccountService, RepositoryService}
import gitbucket.core.ssh.SshServer
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.{AdminAuthenticator, Mailer}
import org.apache.commons.io.IOUtils
import org.json4s.jackson.Serialization
import org.scalatra._
import org.scalatra.forms._
import org.scalatra.i18n.Messages
import scala.collection.JavaConverters._
import scala.collection.mutable.ListBuffer
@@ -70,6 +70,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply)),
"oidcAuthentication" -> trim(label("OIDC", boolean())),
"oidc" -> optionalIfNotChecked("oidcAuthentication", mapping(
"issuer" -> trim(label("Issuer", text(required))),
"clientID" -> trim(label("Client ID", text(required))),
"clientSecret" -> trim(label("Client secret", text(required))),
"jwsAlgorithm" -> trim(label("Signature algorithm", optional(text())))
)(OIDC.apply)),
"skinName" -> trim(label("AdminLTE skin name", text(required)))
)(SystemSettings.apply).verifying { settings =>
Vector(

View File

@@ -0,0 +1,19 @@
package gitbucket.core.model
trait AccountFederationComponent { self: Profile =>
import profile.api._
lazy val AccountFederations = TableQuery[AccountFederations]
class AccountFederations(tag: Tag) extends Table[AccountFederation](tag, "ACCOUNT_FEDERATION") {
val issuer = column[String]("ISSUER")
val subject = column[String]("SUBJECT")
val userName = column[String]("USER_NAME")
def * = (issuer, subject, userName) <> (AccountFederation.tupled, AccountFederation.unapply)
def byPrimaryKey(issuer: String, subject: String): Rep[Boolean] =
(this.issuer === issuer.bind) && (this.subject === subject.bind)
}
}
case class AccountFederation(issuer: String, subject: String, userName: String)

View File

@@ -1,7 +1,7 @@
package gitbucket.core.model
import gitbucket.core.util.DatabaseConfig
import com.github.takezoe.slick.blocking.BlockingJdbcProfile
import gitbucket.core.util.DatabaseConfig
trait Profile {
val profile: BlockingJdbcProfile
@@ -61,6 +61,7 @@ trait CoreProfile extends ProfileProvider with Profile
with RepositoryWebHookEventComponent
with AccountWebHookComponent
with AccountWebHookEventComponent
with AccountFederationComponent
with ProtectedBranchComponent
with DeployKeyComponent
with ReleaseComponent

View File

@@ -0,0 +1,74 @@
package gitbucket.core.service
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.{AccountFederations, Accounts}
import gitbucket.core.model.{Account, AccountFederation}
import gitbucket.core.util.SyntaxSugars.~
import org.slf4j.LoggerFactory
trait AccountFederationService extends AccountService {
private val logger = LoggerFactory.getLogger(classOf[AccountFederationService])
/**
* Get or create a user account federated with OIDC or SAML IdP.
*
* @param issuer Issuer
* @param subject Subject
* @param mailAddress Mail address
* @param preferredUserName Username (if this is none, username will be generated from the mail address)
* @param fullName Fullname (defaults to username)
* @return Account
*/
def getOrCreateFederatedUser(issuer: String,
subject: String,
mailAddress: String,
preferredUserName: Option[String],
fullName: Option[String])(implicit s: Session): Option[Account] =
getAccountByFederation(issuer, subject) match {
case Some(account) if !account.isRemoved =>
Some(account)
case Some(account) =>
logger.info(s"Federated user found but disabled: userName=${account.userName}, isRemoved=${account.isRemoved}")
None
case None =>
findAvailableUserName(preferredUserName, mailAddress) flatMap { userName =>
createAccount(userName, "", fullName.getOrElse(userName), mailAddress, isAdmin = false, None, None)
createAccountFederation(issuer, subject, userName)
getAccountByUserName(userName)
}
}
private def extractSafeStringForUserName(s: String) = """^[a-zA-Z0-9][a-zA-Z0-9\-_.]*""".r.findPrefixOf(s)
/**
* Find an available username from the preferred username or mail address.
*
* @param mailAddress Mail address
* @param preferredUserName Username
* @return Available username
*/
private def findAvailableUserName(preferredUserName: Option[String], mailAddress: String)(implicit s: Session): Option[String] = {
preferredUserName.flatMap(n => extractSafeStringForUserName(n)).orElse(extractSafeStringForUserName(mailAddress)) match {
case Some(safeUserName) =>
getAccountByUserName(safeUserName, includeRemoved = true) match {
case None => Some(safeUserName)
case Some(_) =>
logger.info(s"User ($safeUserName) already exists. preferredUserName=$preferredUserName, mailAddress=$mailAddress")
None
}
case None =>
logger.info(s"Could not extract username from preferredUserName=$preferredUserName, mailAddress=$mailAddress")
None
}
}
def getAccountByFederation(issuer: String, subject: String)(implicit s: Session): Option[Account] =
AccountFederations.filter(_.byPrimaryKey(issuer, subject))
.join(Accounts).on { case af ~ ac => af.userName === ac.userName }
.map { case _ ~ ac => ac }
.firstOption
def createAccountFederation(issuer: String, subject: String, userName: String)(implicit s: Session): Unit =
AccountFederations insert AccountFederation(issuer, subject, userName)
}

View File

@@ -0,0 +1,177 @@
package gitbucket.core.service
import java.net.URI
import com.nimbusds.jose.JOSEException
import com.nimbusds.jose.proc.BadJOSEException
import com.nimbusds.jose.util.DefaultResourceRetriever
import com.nimbusds.oauth2.sdk._
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer, State}
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator
import com.nimbusds.openid.connect.sdk.{AuthenticationErrorResponse, _}
import gitbucket.core.model.Account
import gitbucket.core.model.Profile.profile.blockingApi._
import org.slf4j.LoggerFactory
import scala.collection.JavaConverters.mapAsJavaMap
/**
* Service class for the OpenID Connect authentication.
*/
trait OpenIDConnectService extends AccountFederationService {
private val logger = LoggerFactory.getLogger(classOf[OpenIDConnectService])
private val JWK_REQUEST_TIMEOUT = 5000
private val OIDC_SCOPE = new Scope(
OIDCScopeValue.OPENID,
OIDCScopeValue.EMAIL,
OIDCScopeValue.PROFILE)
/**
* Obtain the OIDC metadata from discovery and create an authentication request.
*
* @param issuer Issuer, used to construct the discovery endpoint URL, e.g. https://accounts.google.com
* @param clientID Client ID (given by the issuer)
* @param redirectURI Redirect URI
* @return Authentication request
*/
def createOIDCAuthenticationRequest(issuer: Issuer,
clientID: ClientID,
redirectURI: URI): AuthenticationRequest = {
val metadata = OIDCProviderMetadata.resolve(issuer)
new AuthenticationRequest(
metadata.getAuthorizationEndpointURI,
new ResponseType(ResponseType.Value.CODE),
OIDC_SCOPE,
clientID,
redirectURI,
new State(),
new Nonce())
}
/**
* Proceed the OpenID Connect authentication.
*
* @param params Query parameters of the authentication response
* @param redirectURI Redirect URI
* @param state State saved in the session
* @param nonce Nonce saved in the session
* @param oidc OIDC settings
* @return ID token
*/
def authenticate(params: Map[String, String],
redirectURI: URI,
state: State,
nonce: Nonce,
oidc: SystemSettingsService.OIDC)(implicit s: Session): Option[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
}
}
}
/**
* Validate the authentication response.
*
* @param params Query parameters of the authentication response
* @param state State saved in the session
* @param redirectURI Redirect URI
* @return Authentication response
*/
def validateOIDCAuthenticationResponse(params: Map[String, String], state: State, redirectURI: URI): Option[AuthenticationSuccessResponse] =
try {
AuthenticationResponseParser.parse(redirectURI, mapAsJavaMap(params)) match {
case response: AuthenticationSuccessResponse =>
if (response.getState == state) {
Some(response)
} else {
logger.info(s"OIDC authentication state did not match: response(${response.getState}) != session($state)")
None
}
case response: AuthenticationErrorResponse =>
logger.info(s"OIDC authentication response has error: ${response.getErrorObject}")
None
}
} catch {
case e: ParseException =>
logger.info(s"OIDC authentication response has error: $e")
None
}
/**
* Obtain the ID token from the OpenID Provider.
*
* @param authorizationCode Authorization code in the query string
* @param nonce Nonce
* @param redirectURI Redirect URI
* @param oidc OIDC settings
* @return Token response
*/
def obtainOIDCToken(authorizationCode: AuthorizationCode,
nonce: Nonce,
redirectURI: URI,
oidc: SystemSettingsService.OIDC): Option[IDTokenClaimsSet] = {
val metadata = OIDCProviderMetadata.resolve(oidc.issuer)
val tokenRequest = new TokenRequest(metadata.getTokenEndpointURI,
new ClientSecretBasic(oidc.clientID, oidc.clientSecret),
new AuthorizationCodeGrant(authorizationCode, redirectURI),
OIDC_SCOPE)
val httpResponse = tokenRequest.toHTTPRequest.send()
try {
OIDCTokenResponseParser.parse(httpResponse) match {
case response: OIDCTokenResponse =>
validateOIDCTokenResponse(response, metadata, nonce, oidc)
case response: TokenErrorResponse =>
logger.info(s"OIDC token response has error: ${response.getErrorObject.toJSONObject}")
None
}
} catch {
case e: ParseException =>
logger.info(s"OIDC token response has error: $e")
None
}
}
/**
* Validate the token response.
*
* @param response Token response
* @param metadata OpenID Provider metadata
* @param nonce Nonce
* @return Claims
*/
def validateOIDCTokenResponse(response: OIDCTokenResponse,
metadata: OIDCProviderMetadata,
nonce: Nonce,
oidc: SystemSettingsService.OIDC): Option[IDTokenClaimsSet] =
Option(response.getOIDCTokens.getIDToken) match {
case Some(jwt) =>
val validator = oidc.jwsAlgorithm map { jwsAlgorithm =>
new IDTokenValidator(metadata.getIssuer, oidc.clientID, jwsAlgorithm, metadata.getJWKSetURI.toURL,
new DefaultResourceRetriever(JWK_REQUEST_TIMEOUT, JWK_REQUEST_TIMEOUT))
} getOrElse {
new IDTokenValidator(metadata.getIssuer, oidc.clientID)
}
try {
Some(validator.validate(jwt, nonce))
} catch {
case e@(_: BadJOSEException | _: JOSEException) =>
logger.info(s"OIDC ID token has error: $e")
None
}
case None =>
logger.info(s"OIDC token response does not have a valid ID token: ${response.toJSONObject}")
None
}
}

View File

@@ -1,11 +1,14 @@
package gitbucket.core.service
import gitbucket.core.util.Implicits._
import javax.servlet.http.HttpServletRequest
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.oauth2.sdk.auth.Secret
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer}
import gitbucket.core.service.SystemSettingsService._
import gitbucket.core.util.ConfigUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.SyntaxSugars._
import SystemSettingsService._
import javax.servlet.http.HttpServletRequest
trait SystemSettingsService {
@@ -54,6 +57,15 @@ trait SystemSettingsService {
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
}
}
props.setProperty(OidcAuthentication, settings.oidcAuthentication.toString)
if (settings.oidcAuthentication) {
settings.oidc.map { oidc =>
props.setProperty(OidcIssuer, oidc.issuer.getValue)
props.setProperty(OidcClientId, oidc.clientID.getValue)
props.setProperty(OidcClientSecret, oidc.clientSecret.getValue)
oidc.jwsAlgorithm.map { x => props.setProperty(OidcJwsAlgorithm, x.getName) }
}
}
props.setProperty(SkinName, settings.skinName.toString)
using(new java.io.FileOutputStream(GitBucketConf)){ out =>
props.store(out, null)
@@ -113,6 +125,17 @@ trait SystemSettingsService {
} else {
None
},
getValue(props, OidcAuthentication, false),
if (getValue(props, OidcAuthentication, false)) {
Some(OIDC(
getValue(props, OidcIssuer, ""),
getValue(props, OidcClientId, ""),
getValue(props, OidcClientSecret, ""),
getOptionValue(props, OidcJwsAlgorithm, None)
))
} else {
None
},
getValue(props, SkinName, "skin-blue")
)
}
@@ -139,6 +162,8 @@ object SystemSettingsService {
smtp: Option[Smtp],
ldapAuthentication: Boolean,
ldap: Option[Ldap],
oidcAuthentication: Boolean,
oidc: Option[OIDC],
skinName: String){
def baseUrl(request: HttpServletRequest): String = baseUrl.fold {
@@ -166,6 +191,16 @@ object SystemSettingsService {
ssl: Option[Boolean],
keystore: Option[String])
case class OIDC(
issuer: Issuer,
clientID: ClientID,
clientSecret: Secret,
jwsAlgorithm: Option[JWSAlgorithm])
object OIDC {
def apply(issuer: String, clientID: String, clientSecret: String, jwsAlgorithm: Option[String]): OIDC =
new OIDC(new Issuer(issuer), new ClientID(clientID), new Secret(clientSecret), jwsAlgorithm.map(JWSAlgorithm.parse))
}
case class Smtp(
host: String,
port: Option[Int],
@@ -221,6 +256,11 @@ object SystemSettingsService {
private val LdapTls = "ldap.tls"
private val LdapSsl = "ldap.ssl"
private val LdapKeystore = "ldap.keystore"
private val OidcAuthentication = "oidc_authentication"
private val OidcIssuer = "oidc.issuer"
private val OidcClientId = "oidc.client_id"
private val OidcClientSecret = "oidc.client_secret"
private val OidcJwsAlgorithm = "oidc.jws_algorithm"
private val SkinName = "skinName"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {

View File

@@ -25,6 +25,21 @@ object Keys {
*/
val DashboardPulls = "dashboard/pulls"
/**
* Session key for the OpenID Connect authentication.
*/
val OidcState = "oidc/state"
/**
* Session key for the OpenID Connect authentication.
*/
val OidcNonce = "oidc/nonce"
/**
* Session key for the redirect back to after SSO.
*/
val OidcRedirectBackURI = "oidc/redirectBackURI"
/**
* Generate session key for the issue search condition.
*/

View File

@@ -0,0 +1,15 @@
package gitbucket.core.util
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.JWSAlgorithm.Family
import scala.collection.JavaConverters.asScalaSet
object OpenIDConnectUtil {
val JWS_ALGORITHMS: Map[String, Set[JWSAlgorithm]] = Seq(
"HMAC" -> Family.HMAC_SHA,
"RSA" -> Family.RSA,
"ECDSA" -> Family.EC,
"EdDSA" -> Family.ED
).toMap.map { case (name, family) => (name, asScalaSet(family).toSet) }
}

View File

@@ -1,6 +1,5 @@
@(info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.util.DatabaseConfig
@import gitbucket.core.view.helpers
@import gitbucket.core.util.{DatabaseConfig, OpenIDConnectUtil}
@gitbucket.core.html.main("System settings"){
@gitbucket.core.admin.html.menu("system"){
@gitbucket.core.helper.html.information(info)
@@ -287,6 +286,56 @@
</div>
</div>
</div>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="oidcAuthentication" name="oidcAuthentication"@if(context.settings.oidc){ checked} />
OpenID Connect
</label>
</fieldset>
<div class="oidc">
<div class="form-group">
<label class="control-label col-md-2" for="oidcIssuer">Issuer</label>
<div class="col-md-10">
<input type="text" id="oidcIssuer" name="oidc.issuer" class="form-control" value="@context.settings.oidc.map(_.issuer.getValue)"/>
<span id="error-oidc_issuer" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="oidcClientID">Client ID</label>
<div class="col-md-10">
<input type="text" id="oidcClientID" name="oidc.clientID" class="form-control" value="@context.settings.oidc.map(_.clientID.getValue)"/>
<span id="error-oidc_clientID" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="oidcClientID">Client secret</label>
<div class="col-md-10">
<input type="password" id="oidcClientSecret" name="oidc.clientSecret" class="form-control" value="@context.settings.oidc.map(_.clientSecret.getValue)"/>
<span id="error-oidc_clientSecret" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="oidcJwsAlgorithm">Expected signature</label>
<div class="col-md-10">
<select id="oidcJwsAlgorithm" name="oidc.jwsAlgorithm" class="form-control">
<option value="" @if(context.settings.oidc.flatMap(_.jwsAlgorithm) == None){selected}>
No signature
</option>
@OpenIDConnectUtil.JWS_ALGORITHMS.map { case (family, algorithms) =>
<optgroup label="@family">
@algorithms.map { algorithm =>
<option value="@algorithm.getName" @if(context.settings.oidc.flatMap(_.jwsAlgorithm) == Some(algorithm)){selected}>
@algorithm.getName
</option>
}
</optgroup>
}
</select>
<span class="muted">Choose the expected signature algorithm of the token response. Most IdP provides RS256 or HS256.</span>
<span id="error-oidc_jwsAlgorithm" class="error"></span>
</div>
</div>
</div>
<!--====================================================================-->
<!-- Notification email -->
<!--====================================================================-->
@@ -456,5 +505,9 @@ $(function(){
$('#ldapAuthentication').change(function(){
$('.ldap input').prop('disabled', !$(this).prop('checked'));
}).change();
$('#oidcAuthentication').change(function(){
$('.oidc input, .oidc select').prop('disabled', !$(this).prop('checked'));
}).change();
});
</script>

View File

@@ -4,6 +4,15 @@
<div class="panel panel-default">
<div class="panel-heading strong">Sign in</div>
<ul class="list-group list-group-flush">
@if(context.settings.oidcAuthentication){
<li class="list-group-item">
<form action="@context.path/signin/oidc" method="POST">
<input type="hidden" name="hash"/>
<input type="submit" class="btn btn-success" value="Sign in with OpenID Connect"
onClick="this.form.hash.value = window.location.hash;"/>
</form>
</li>
}
<li class="list-group-item">
<form action="@context.path/signin" method="POST" validate="true">
<div class="form-group">

View File

@@ -2,17 +2,16 @@ package gitbucket.core.view
import java.text.SimpleDateFormat
import java.util.Date
import gitbucket.core.model.Account
import gitbucket.core.service.{RequestCache, SystemSettingsService}
import gitbucket.core.controller.Context
import SystemSettingsService.SystemSettings
import javax.servlet.http.{HttpServletRequest, HttpSession}
import play.twirl.api.Html
import gitbucket.core.controller.Context
import gitbucket.core.model.Account
import gitbucket.core.service.RequestCache
import gitbucket.core.service.SystemSettingsService.SystemSettings
import org.mockito.Mockito._
import org.scalatest.FunSpec
import org.scalatest.mockito.MockitoSugar
import org.mockito.Mockito._
import play.twirl.api.Html
class AvatarImageProviderSpec extends FunSpec with MockitoSugar {
@@ -119,6 +118,8 @@ class AvatarImageProviderSpec extends FunSpec with MockitoSugar {
smtp = None,
ldapAuthentication = false,
ldap = None,
oidcAuthentication = false,
oidc = None,
skinName = "skin-blue"
)