mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
Implement authentication via OpenIdConnect and OAuth2
The new subproject "oidc" handles all the details for working with an OpenID Connect provider (like keycloak) or only OAuth2 - only supporting the "Authorization Code Flow" for both variants.
This commit is contained in:
@ -61,6 +61,44 @@ docspell.server {
|
||||
}
|
||||
}
|
||||
|
||||
# Configures OpenID Connect or OAuth2 authentication. Only the
|
||||
# "Authorization Code Flow" is supported.
|
||||
#
|
||||
# When using OpenID Connect, a scope is mandatory.
|
||||
# TODO
|
||||
#
|
||||
# Below are examples for OpenID Connect (keycloak) and OAuth2
|
||||
# (github).
|
||||
openid =
|
||||
[ { enabled = false,
|
||||
provider = {
|
||||
provider-id = "keycloak",
|
||||
client-id = "docspell",
|
||||
client-secret = "21cd4550-6328-439e-bf06-911e4cdd56a6",
|
||||
scope = "docspell",
|
||||
authorize-url = "http://localhost:8080/auth/realms/home/protocol/openid-connect/auth",
|
||||
token-url = "http://localhost:8080/auth/realms/home/protocol/openid-connect/token",
|
||||
#User URL is not used when signature key is set.
|
||||
#user-url = "http://localhost:8080/auth/realms/home/protocol/openid-connect/userinfo",
|
||||
sign-key = "b64:MII…ZYL09vAwLn8EAcSkCAwEAAQ==",
|
||||
sig-algo = "RS512"
|
||||
}
|
||||
},
|
||||
{ enabled = false,
|
||||
provider = {
|
||||
provider-id = "github",
|
||||
client-id = "<your github client id>",
|
||||
client-secret = "<your github client secret>",
|
||||
scope = "",
|
||||
authorize-url = "https://github.com/login/oauth/authorize",
|
||||
token-url = "https://github.com/login/oauth/access_token",
|
||||
user-url = "https://api.github.com/user",
|
||||
sign-key = ""
|
||||
sig-algo = "RS256" #unused but must be set to something
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# This endpoint allows to upload files to any collective. The
|
||||
# intention is that local software integrates with docspell more
|
||||
# easily. Therefore the endpoint is not protected by the usual
|
||||
|
@ -10,6 +10,8 @@ import docspell.backend.auth.Login
|
||||
import docspell.backend.{Config => BackendConfig}
|
||||
import docspell.common._
|
||||
import docspell.ftssolr.SolrConfig
|
||||
import docspell.oidc.ProviderConfig
|
||||
import docspell.restserver.Config.OpenIdConfig
|
||||
|
||||
import com.comcast.ip4s.IpAddress
|
||||
|
||||
@ -25,7 +27,8 @@ case class Config(
|
||||
maxItemPageSize: Int,
|
||||
maxNoteLength: Int,
|
||||
fullTextSearch: Config.FullTextSearch,
|
||||
adminEndpoint: Config.AdminEndpoint
|
||||
adminEndpoint: Config.AdminEndpoint,
|
||||
openid: List[OpenIdConfig]
|
||||
)
|
||||
|
||||
object Config {
|
||||
@ -70,4 +73,6 @@ object Config {
|
||||
|
||||
object FullTextSearch {}
|
||||
|
||||
final case class OpenIdConfig(enabled: Boolean, provider: ProviderConfig)
|
||||
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ package docspell.restserver
|
||||
|
||||
import docspell.backend.signup.{Config => SignupConfig}
|
||||
import docspell.common.config.Implicits._
|
||||
import docspell.oidc.SignatureAlgo
|
||||
|
||||
import pureconfig._
|
||||
import pureconfig.generic.auto._
|
||||
@ -21,5 +22,8 @@ object ConfigFile {
|
||||
object Implicits {
|
||||
implicit val signupModeReader: ConfigReader[SignupConfig.Mode] =
|
||||
ConfigReader[String].emap(reason(SignupConfig.Mode.fromString))
|
||||
|
||||
implicit val sigAlgoReader: ConfigReader[SignatureAlgo] =
|
||||
ConfigReader[String].emap(reason(SignatureAlgo.fromString))
|
||||
}
|
||||
}
|
||||
|
@ -12,12 +12,16 @@ import fs2.Stream
|
||||
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common._
|
||||
import docspell.oidc.CodeFlowRoutes
|
||||
import docspell.restserver.auth.OpenId
|
||||
import docspell.restserver.http4s.EnvMiddleware
|
||||
import docspell.restserver.routes._
|
||||
import docspell.restserver.webapp._
|
||||
|
||||
import org.http4s._
|
||||
import org.http4s.blaze.client.BlazeClientBuilder
|
||||
import org.http4s.blaze.server.BlazeServerBuilder
|
||||
import org.http4s.client.Client
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.headers.Location
|
||||
import org.http4s.implicits._
|
||||
@ -33,9 +37,10 @@ object RestServer {
|
||||
restApp <-
|
||||
RestAppImpl
|
||||
.create[F](cfg, pools.connectEC, pools.httpClientEC)
|
||||
httpClient <- BlazeClientBuilder[F](pools.httpClientEC).resource
|
||||
httpApp = Router(
|
||||
"/api/info" -> routes.InfoRoutes(),
|
||||
"/api/v1/open/" -> openRoutes(cfg, restApp),
|
||||
"/api/v1/open/" -> openRoutes(cfg, httpClient, restApp),
|
||||
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
|
||||
securedRoutes(cfg, restApp, token)
|
||||
},
|
||||
@ -98,8 +103,17 @@ object RestServer {
|
||||
"clientSettings" -> ClientSettingsRoutes(restApp.backend, token)
|
||||
)
|
||||
|
||||
def openRoutes[F[_]: Async](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||
def openRoutes[F[_]: Async](
|
||||
cfg: Config,
|
||||
client: Client[F],
|
||||
restApp: RestApp[F]
|
||||
): HttpRoutes[F] =
|
||||
Router(
|
||||
"auth/oauth" -> CodeFlowRoutes(
|
||||
OpenId.handle[F](restApp.backend, cfg),
|
||||
OpenId.codeFlowConfig(cfg),
|
||||
client
|
||||
),
|
||||
"auth" -> LoginRoutes.login(restApp.backend.login, cfg),
|
||||
"signup" -> RegisterRoutes(restApp.backend, cfg),
|
||||
"upload" -> UploadRoutes.open(restApp.backend, cfg),
|
||||
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2020 Docspell Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restserver.auth
|
||||
|
||||
import cats.effect._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.oidc.{CodeFlowConfig, OnUserInfo}
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.http4s.ClientRequestInfo
|
||||
|
||||
object OpenId {
|
||||
|
||||
def codeFlowConfig[F[_]](config: Config): CodeFlowConfig[F] =
|
||||
CodeFlowConfig(
|
||||
req =>
|
||||
ClientRequestInfo
|
||||
.getBaseUrl(config, req) / "api" / "v1" / "open" / "auth" / "oauth",
|
||||
id =>
|
||||
config.openid.filter(_.enabled).find(_.provider.providerId == id).map(_.provider)
|
||||
)
|
||||
|
||||
def handle[F[_]: Async](backend: BackendApp[F], cfg: Config): OnUserInfo[F] =
|
||||
OnUserInfo((req, provider, userInfo) =>
|
||||
userInfo match {
|
||||
case Some(userJson) =>
|
||||
println(s"$backend $cfg")
|
||||
OnUserInfo.logInfo[F].handle(req, provider, Some(userJson))
|
||||
case None =>
|
||||
OnUserInfo.logInfo[F].handle(req, provider, None)
|
||||
}
|
||||
)
|
||||
|
||||
// u <- userInfo
|
||||
// newAcc <- OptionT.liftF(
|
||||
// NewAccount.create(u ++ Ident.unsafe(":") ++ p.providerId, AccountSource.OAuth(p.id.id))
|
||||
// )
|
||||
// acc <- OptionT.liftF(S.account.createIfMissing(newAcc))
|
||||
// accId = acc.accountId(None)
|
||||
// _ <- OptionT.liftF(S.account.updateLoginStats(accId))
|
||||
// token <- OptionT.liftF(
|
||||
// AuthToken.user[F](accId, cfg.backend.auth.serverSecret)
|
||||
// )
|
||||
// } yield token
|
||||
//
|
||||
// val uri = getBaseUrl( req).withQuery("oauth", "1") / "app" / "login"
|
||||
// val location = Location(Uri.unsafeFromString(uri.asString))
|
||||
// userId.value.flatMap {
|
||||
// case Some(t) =>
|
||||
// TemporaryRedirect(location)
|
||||
// .map(_.addCookie(CookieData(t).asCookie(getBaseUrl(req))))
|
||||
// case None => TemporaryRedirect(location)
|
||||
// }
|
||||
}
|
Reference in New Issue
Block a user