Skip to content

Commit

Permalink
Merge pull request #1295 from eikek/proxy-auth
Browse files Browse the repository at this point in the history
Proxy auth
  • Loading branch information
mergify[bot] authored Jan 6, 2024
2 parents 57598fb + f5f3abb commit 2e40b00
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,37 @@ case class AuthConfig(
http: AuthConfig.Http,
httpBasic: AuthConfig.HttpBasic,
command: AuthConfig.Command,
proxy: AuthConfig.Proxy,
internal: AuthConfig.Internal,
oauth: Seq[AuthConfig.OAuth]
) {

def isOAuthOnly: Boolean =
fixed.disabled && http.disabled &&
httpBasic.disabled && command.disabled &&
internal.disabled && oauth.nonEmpty
internal.disabled && proxy.disabled && oauth.exists(_.enabled)

def isProxyAuthOnly: Boolean =
fixed.disabled && http.disabled &&
httpBasic.disabled && command.disabled &&
internal.disabled && !oauth.exists(_.enabled) && proxy.enabled

def isAutoLogin: Boolean =
fixed.disabled && http.disabled &&
httpBasic.disabled && command.disabled &&
internal.disabled && (oauth.exists(_.enabled) || proxy.enabled)
}

object AuthConfig {

final case class Proxy(
enabled: Boolean,
userHeader: String,
emailHeader: Option[String]
) {
def disabled = !enabled
}

case class Fixed(enabled: Boolean, user: Ident, password: Password, order: Int) {
def disabled = !enabled
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class LoginModuleTest extends FunSuite {
AuthConfig.Http(true, LenientUri.unsafe("http://test.com"), "GET", "", "", 2),
AuthConfig.HttpBasic(true, LenientUri.unsafe("http://test.com"), "GET", 3),
AuthConfig.Command(true, Seq.empty, 0, 4),
AuthConfig.Proxy(false, "", None),
AuthConfig.Internal(true, 5),
Seq.empty
)
Expand Down
17 changes: 17 additions & 0 deletions modules/microsite/docs/doc/configure.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,23 @@ The internal login module simply authenticates against the sharry
database. If it is disabled, you should disable signup, too, because those
user won't be authenticated.

#### Proxy

The `proxy` option allows automatically authenticate users by trusting
specific request headers. The configured headers of the login request
to `open/auth/proxy` are read and a user account is created if
missing. Be aware that sharry blindly trusts these headers.

```
proxy {
enabled = false
user-header = "X-Valid-User"
email-header = "X-User-Email"
}
```

The webapp automatically logs the user in, if the auth configuration
only consists of proxy auth and nothing else.

#### OAuth

Expand Down
23 changes: 23 additions & 0 deletions modules/restapi/src/main/resources/sharry-openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,20 @@ paths:
description: OK
'403':
description: Forbidden
/open/auth/proxy:
post:
operationId: "open-auth-proxy"
tags: [ Authentication ]
summary: Authenticate via request headers
description: |
When using an authenticating proxy in front, this login route
can be used to rely on trusted request headers to perform
login.
responses:
'200':
description: Ok
'403':
description: Forbidden
/sec/auth/session:
post:
operationId: "sec-auth-session"
Expand Down Expand Up @@ -1923,6 +1937,9 @@ components:
- defaultValidity
- initialTheme
- oauthAutoRedirect
- proxyAuthEnabled
- proxyOnly
- hideLoginForm
properties:
appName:
type: string
Expand Down Expand Up @@ -1990,6 +2007,12 @@ components:
format: theme
oauthAutoRedirect:
type: boolean
proxyAuthEnabled:
type: boolean
proxyOnly:
type: boolean
hideLoginForm:
type: boolean
OAuthItem:
description: |
Information about a configured OAuth provider.
Expand Down
23 changes: 17 additions & 6 deletions modules/restserver/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,11 @@ sharry.restserver {
# The inital ui theme to use. Can be either 'light' or 'dark'.
initial-theme = "light"

# When only OAuth is configured and only a single provider, then
# the weapp automatically redirects to its authentication page
# skipping the sharry login page. This will also disable the
# logout button, since sharry is not in charge anyways.
# When only OAuth (or only Proxy Auth) is configured and only a
# single provider, then the weapp automatically redirects to its
# authentication page skipping the sharry login page. This will
# also disable the logout button, since sharry is not in charge
# anyways.
oauth-auto-redirect = true

# A custom html snippet that is rendered into the html head
Expand Down Expand Up @@ -265,8 +266,8 @@ sharry.restserver {
id = "aad"
name = "Azure AD"
icon = "fab fa-microsoft"
scope = ""
authorize-url = "https://login.microsoftonline.com/<your tenant ID>/oauth2/v2.0/authorize?scope=openid"
scope = "openid"
authorize-url = "https://login.microsoftonline.com/<your tenant ID>/oauth2/v2.0/authorize"
token-url = "https://login.microsoftonline.com/<your tenant ID>/oauth2/v2.0/token"
user-url = "https://graph.microsoft.com/oidc/userinfo"
user-id-key = "email"
Expand All @@ -275,6 +276,16 @@ sharry.restserver {
client-secret = "<your client secret>"
}
]

# Allows to inspect the request headers for finding already
# authorized user name/email. If enabled and during login the
# request contains these headers, they will be used to
# automatically create accounts.
proxy {
enabled = false
user-header = "X-Valid-User"
email-header = "X-User-Email"
}
}

# The database connection.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ object InfoRoutes {
cfg.aliasMemberEnabled,
cfg.webapp.defaultValidity,
cfg.webapp.initialTheme,
cfg.webapp.oauthAutoRedirect
cfg.webapp.oauthAutoRedirect,
cfg.backend.auth.proxy.enabled,
cfg.backend.auth.isProxyAuthOnly,
cfg.backend.auth.isAutoLogin
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.http4s.circe.CirceEntityEncoder._
import org.http4s.client.Client
import org.http4s.dsl.Http4sDsl
import org.http4s.headers.Location
import org.typelevel.ci.CIString

object LoginRoutes {

Expand All @@ -42,6 +43,51 @@ object LoginRoutes {
resp <- makeResponse(dsl, cfg, req, res, up.account)
} yield resp

case req @ POST -> Root / "proxy" =>
val unameOpt =
req.headers
.get(CIString(cfg.backend.auth.proxy.userHeader))
.map(_.head.value)
.filter(_ => cfg.backend.auth.proxy.enabled)

val email = cfg.backend.auth.proxy.emailHeader
.map(CIString.apply)
.flatMap(req.headers.get(_).map(_.head.value))

def doLogin(userId: Ident) = for {
newAcc <- NewAccount.create(userId, AccountSource.Extern, email = email)
token <- finalizeLogin(cfg, S)(newAcc)
resp <- makeResponse(dsl, cfg, req, LoginResult.ok(token), userId.id)
} yield resp

for {
_ <-
if (cfg.backend.auth.proxy.disabled)
logger.info("Proxy authentication is disabled in the config!")
else logger.debug(s"Use proxy authentication: user=$unameOpt, email=$email")

resp <- unameOpt.map(Ident.apply) match {
case None =>
makeResponse(
dsl,
cfg,
req,
LoginResult.invalidAuth,
unameOpt.getOrElse("<no-user-id>")
)
case Some(Left(err)) =>
logger.error(s"Error reading username from header: $err") >>
makeResponse(
dsl,
cfg,
req,
LoginResult.invalidAuth,
unameOpt.getOrElse("<no-user-id>")
)
case Some(Right(userId)) => doLogin(userId)
}
} yield resp

case req @ GET -> Root / "oauth" / id =>
findOAuthProvider(cfg, id) match {
case Some(p) =>
Expand Down Expand Up @@ -94,12 +140,7 @@ object LoginRoutes {
email = u.email
)
)
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)
)
token <- OptionT.liftF(finalizeLogin(cfg, S)(newAcc))
} yield token

val uri = getBaseUrl(cfg, req).withQuery("oauth", "1") / "app" / "login"
Expand All @@ -113,6 +154,16 @@ object LoginRoutes {
}
}

private def finalizeLogin[F[_]: Async](cfg: Config, S: BackendApp[F])(
newAcc: NewAccount
) =
for {
acc <- S.account.createIfMissing(newAcc)
accId = acc.accountId(None)
_ <- S.account.updateLoginStats(accId)
token <- AuthToken.user[F](accId, cfg.backend.auth.serverSecret)
} yield token

private def redirectUri[F[_]](
cfg: Config,
req: Request[F],
Expand Down
10 changes: 10 additions & 0 deletions modules/webapp/src/main/elm/Api.elm
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module Api exposing
, listAliasMember
, loadAccount
, login
, loginProxy
, loginSession
, logout
, modifyAccount
Expand Down Expand Up @@ -537,6 +538,15 @@ login flags up receive =
}


loginProxy : Flags -> (Result Http.Error AuthResult -> msg) -> Cmd msg
loginProxy flags receive =
Http.post
{ url = flags.config.baseUrl ++ "/api/v2/open/auth/proxy"
, body = Http.emptyBody
, expect = Http.expectJson receive Api.Model.AuthResult.decoder
}


logout : Flags -> (Result Http.Error () -> msg) -> Cmd msg
logout flags receive =
Http2.authPost
Expand Down
6 changes: 3 additions & 3 deletions modules/webapp/src/main/elm/App/View.elm
Original file line number Diff line number Diff line change
Expand Up @@ -265,13 +265,13 @@ userMenu2 texts model acc =
, a
[ href "#"
, class dropdownItem
, classList [ ( "disabled", Data.Flags.isOAuthAutoRedirect model.flags ) ]
, if Data.Flags.isOAuthAutoRedirect model.flags then
, classList [ ( "disabled", Data.Flags.isAutoRedirect model.flags ) ]
, if Data.Flags.isAutoRedirect model.flags then
class ""

else
onClick Logout
, if Data.Flags.isOAuthAutoRedirect model.flags then
, if Data.Flags.isAutoRedirect model.flags then
title texts.app.logoutOAuth

else
Expand Down
10 changes: 10 additions & 0 deletions modules/webapp/src/main/elm/Data/Flags.elm
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ isOAuthAutoRedirect flags =
flags.config.oauthAutoRedirect && flags.config.oauthOnly


isProxyAutoRedirect : Flags -> Bool
isProxyAutoRedirect flags =
flags.config.oauthAutoRedirect && flags.config.proxyOnly


isAutoRedirect : Flags -> Bool
isAutoRedirect flags =
isOAuthAutoRedirect flags || isProxyAutoRedirect flags


getToken : Flags -> Maybe String
getToken flags =
flags.account
Expand Down
1 change: 1 addition & 0 deletions modules/webapp/src/main/elm/Page/Login/Data.elm
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Msg
= SetUsername String
| SetPassword String
| Authenticate
| AuthenticateProxy
| AuthResp (Result Http.Error AuthResult)
| Init
| LangChooseMsg Comp.LanguageChoose.Msg
6 changes: 6 additions & 0 deletions modules/webapp/src/main/elm/Page/Login/Update.elm
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ update ( referrer, oauth ) flags key msg model =
_ ->
( model, Cmd.none, Nothing )

else if not oauth && Data.Flags.isProxyAutoRedirect flags && flags.account == Nothing then
( model, Api.loginProxy flags AuthResp, Nothing )

else
( model, Cmd.none, Nothing )

Expand All @@ -43,6 +46,9 @@ update ( referrer, oauth ) flags key msg model =
Authenticate ->
( model, Api.login flags (UserPass model.username model.password) AuthResp, Nothing )

AuthenticateProxy ->
( model, Api.loginProxy flags AuthResp, Nothing )

AuthResp (Ok lr) ->
if lr.success then
loginSuccess referrer flags lr model
Expand Down
24 changes: 22 additions & 2 deletions modules/webapp/src/main/elm/Page/Login/View.elm
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Data.Flags exposing (Flags)
import Data.UiTheme exposing (UiTheme)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput, onSubmit)
import Html.Events exposing (onClick, onInput, onSubmit)
import Markdown
import Messages.LoginPage exposing (Texts)
import Page exposing (Page(..))
Expand Down Expand Up @@ -39,7 +39,7 @@ view texts flags currentTheme model =
, onSubmit Authenticate
, autocomplete False
, classList
[ ( "hidden invisible", flags.config.oauthOnly ) ]
[ ( "hidden invisible", flags.config.hideLoginForm ) ]
]
[ div [ class "flex flex-col mt-6" ]
[ label
Expand Down Expand Up @@ -101,6 +101,7 @@ view texts flags currentTheme model =

else
renderOAuthButtons texts flags model
, renderProxyAuthButton texts
, resultMessage texts model
, renderLangAndSignup texts flags
]
Expand Down Expand Up @@ -189,6 +190,25 @@ renderOAuthButton texts flags item =
]


renderProxyAuthButton : Texts -> Html Msg
renderProxyAuthButton texts =
div
[ class "flex flex-row space-x-2 flex-wrap items-center justify-center"
]
[ a
[ class S.primaryBasicButton
, class "mt-1"
, href "#"
, onClick AuthenticateProxy
]
[ i [ class "fa fa-right-to-bracket" ] []
, span [ class "ml-2" ]
[ text texts.loginButton
]
]
]


resultMessage : Texts -> Model -> Html Msg
resultMessage texts model =
case model.result of
Expand Down

0 comments on commit 2e40b00

Please sign in to comment.