Fix OTP authentication for external accounts

This commit is contained in:
eikek 2021-09-06 01:07:31 +02:00
parent 8158e36d40
commit 468ba90158
12 changed files with 95 additions and 46 deletions

View File

@ -9,7 +9,9 @@ package docspell.oidc
import cats.data.{Kleisli, OptionT} import cats.data.{Kleisli, OptionT}
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
import docspell.common._ import docspell.common._
import org.http4s.HttpRoutes import org.http4s.HttpRoutes
import org.http4s._ import org.http4s._
import org.http4s.client.Client import org.http4s.client.Client

View File

@ -78,8 +78,8 @@ object Config {
object FullTextSearch {} object FullTextSearch {}
final case class OpenIdConfig( final case class OpenIdConfig(
enabled: Boolean, enabled: Boolean,
display: String, display: String,
collectiveKey: OpenId.UserInfo.Extractor, collectiveKey: OpenId.UserInfo.Extractor,
userKey: String, userKey: String,
provider: ProviderConfig provider: ProviderConfig

View File

@ -54,7 +54,9 @@ object OpenId {
extractColl match { extractColl match {
case ExtractResult.Failure(message) => case ExtractResult.Failure(message) =>
logger.warn(s"Can't retrieve user data using collective-key=${cfg.collectiveKey.asString}: $message") *> logger.warn(
s"Can't retrieve user data using collective-key=${cfg.collectiveKey.asString}: $message"
) *>
TemporaryRedirect(location) TemporaryRedirect(location)
case ExtractResult.Account(accountId) => case ExtractResult.Account(accountId) =>
@ -63,7 +65,9 @@ object OpenId {
case ExtractResult.Identifier(coll) => case ExtractResult.Identifier(coll) =>
Extractor.Lookup(cfg.userKey).find(userJson) match { Extractor.Lookup(cfg.userKey).find(userJson) match {
case ExtractResult.Failure(message) => case ExtractResult.Failure(message) =>
logger.warn(s"Can't retrieve user data using user-key=${cfg.userKey}: $message") *> logger.warn(
s"Can't retrieve user data using user-key=${cfg.userKey}: $message"
) *>
TemporaryRedirect(location) TemporaryRedirect(location)
case ExtractResult.Identifier(name) => case ExtractResult.Identifier(name) =>
@ -144,7 +148,15 @@ object OpenId {
login <- backend.login.loginExternal(config.auth)(accountId) login <- backend.login.loginExternal(config.auth)(accountId)
resp <- login match { resp <- login match {
case Login.Result.Ok(session, _) => case Login.Result.Ok(session, _) =>
TemporaryRedirect(location) val loc =
if (session.requireSecondFactor)
location.copy(uri =
location.uri
.withQueryParam("openid", "2")
.withQueryParam("auth", session.asString)
)
else location
TemporaryRedirect(loc)
.map(_.addCookie(CookieData(session).asCookie(baseUrl))) .map(_.addCookie(CookieData(session).asCookie(baseUrl)))
case failed => case failed =>

View File

@ -9,6 +9,7 @@ package docspell.restserver.webapp
import docspell.backend.signup.{Config => SignupConfig} import docspell.backend.signup.{Config => SignupConfig}
import docspell.common.{Ident, LenientUri} import docspell.common.{Ident, LenientUri}
import docspell.restserver.{BuildInfo, Config} import docspell.restserver.{BuildInfo, Config}
import io.circe._ import io.circe._
import io.circe.generic.semiauto._ import io.circe.generic.semiauto._
import yamusca.implicits._ import yamusca.implicits._

View File

@ -83,10 +83,7 @@ init key url flags_ settings =
Page.CollectiveSettings.Data.init flags Page.CollectiveSettings.Data.init flags
( loginm, loginc ) = ( loginm, loginc ) =
Page.Login.Data.init flags Page.Login.Data.init flags (Page.loginPageReferrer page)
(Page.loginPageReferrer page
|> Tuple.second
)
homeViewMode = homeViewMode =
if settings.searchMenuVisible then if settings.searchMenuVisible then

View File

@ -158,7 +158,7 @@ updateWithSub msg model =
LogoutResp _ -> LogoutResp _ ->
( { model | loginModel = Page.Login.Data.emptyModel } ( { model | loginModel = Page.Login.Data.emptyModel }
, Page.goto (LoginPage ( Nothing, False )) , Page.goto (LoginPage Page.emptyLoginData)
, Sub.none , Sub.none
) )

View File

@ -6,7 +6,9 @@
module Page exposing module Page exposing
( Page(..) ( LoginData
, Page(..)
, emptyLoginData
, fromUrl , fromUrl
, goto , goto
, hasSidebar , hasSidebar
@ -31,9 +33,24 @@ import Url.Parser.Query as Query
import Util.Maybe import Util.Maybe
type alias LoginData =
{ referrer : Maybe Page
, session : Maybe String
, openid : Int
}
emptyLoginData : LoginData
emptyLoginData =
{ referrer = Nothing
, session = Nothing
, openid = 0
}
type Page type Page
= HomePage = HomePage
| LoginPage ( Maybe Page, Bool ) | LoginPage LoginData
| ManageDataPage | ManageDataPage
| CollectiveSettingPage | CollectiveSettingPage
| UserSettingPage | UserSettingPage
@ -99,10 +116,10 @@ loginPage : Page -> Page
loginPage p = loginPage p =
case p of case p of
LoginPage _ -> LoginPage _ ->
LoginPage ( Nothing, False ) LoginPage emptyLoginData
_ -> _ ->
LoginPage ( Just p, False ) LoginPage { emptyLoginData | referrer = Just p }
pageName : Page -> String pageName : Page -> String
@ -144,14 +161,14 @@ pageName page =
"Item" "Item"
loginPageReferrer : Page -> ( Maybe Page, Bool ) loginPageReferrer : Page -> LoginData
loginPageReferrer page = loginPageReferrer page =
case page of case page of
LoginPage ( r, flag ) -> LoginPage data ->
( r, flag ) data
_ -> _ ->
( Nothing, False ) emptyLoginData
uploadId : Page -> Maybe String uploadId : Page -> Maybe String
@ -170,8 +187,8 @@ pageToString page =
HomePage -> HomePage ->
"/app/home" "/app/home"
LoginPage ( referer, _ ) -> LoginPage data ->
case referer of case data.referrer of
Just (LoginPage _) -> Just (LoginPage _) ->
"/app/login" "/app/login"
@ -280,14 +297,19 @@ fromString str =
fromUrl url fromUrl url
loginPageOAuthQuery : Query.Parser Bool loginPageOAuthQuery : Query.Parser Int
loginPageOAuthQuery = loginPageOAuthQuery =
Query.map Util.Maybe.nonEmpty (Query.string "openid") Query.map (Maybe.withDefault 0) (Query.int "openid")
loginPageParser : Query.Parser ( Maybe Page, Bool ) loginPageSessionQuery : Query.Parser (Maybe String)
loginPageSessionQuery =
Query.string "auth"
loginPageParser : Query.Parser LoginData
loginPageParser = loginPageParser =
Query.map2 Tuple.pair pageQuery loginPageOAuthQuery Query.map3 LoginData pageQuery loginPageSessionQuery loginPageOAuthQuery
pageQuery : Query.Parser (Maybe Page) pageQuery : Query.Parser (Maybe Page)

View File

@ -18,7 +18,7 @@ import Api
import Api.Model.AuthResult exposing (AuthResult) import Api.Model.AuthResult exposing (AuthResult)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Http import Http
import Page exposing (Page(..)) import Page exposing (LoginData, Page(..))
type alias Model = type alias Model =
@ -40,7 +40,7 @@ type FormState
type AuthStep type AuthStep
= StepLogin = StepLogin
| StepOtp AuthResult | StepOtp String
emptyModel : Model emptyModel : Model
@ -54,11 +54,11 @@ emptyModel =
} }
init : Flags -> Bool -> ( Model, Cmd Msg ) init : Flags -> LoginData -> ( Model, Cmd Msg )
init flags oauth = init flags ld =
let let
cmd = cmd =
if oauth then if ld.openid > 0 then
Api.loginSession flags AuthResp Api.loginSession flags AuthResp
else else
@ -74,4 +74,4 @@ type Msg
| Authenticate | Authenticate
| AuthResp (Result Http.Error AuthResult) | AuthResp (Result Http.Error AuthResult)
| SetOtp String | SetOtp String
| AuthOtp AuthResult | AuthOtp String

View File

@ -10,13 +10,13 @@ module Page.Login.Update exposing (update)
import Api import Api
import Api.Model.AuthResult exposing (AuthResult) import Api.Model.AuthResult exposing (AuthResult)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Page exposing (Page(..)) import Page exposing (LoginData, Page(..))
import Page.Login.Data exposing (..) import Page.Login.Data exposing (..)
import Ports import Ports
update : ( Maybe Page, Bool ) -> Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe AuthResult ) update : LoginData -> Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe AuthResult )
update ( referrer, oauth ) flags msg model = update loginData flags msg model =
case msg of case msg of
SetUsername str -> SetUsername str ->
( { model | username = str }, Cmd.none, Nothing ) ( { model | username = str }, Cmd.none, Nothing )
@ -40,11 +40,11 @@ update ( referrer, oauth ) flags msg model =
in in
( model, Api.login flags userPass AuthResp, Nothing ) ( model, Api.login flags userPass AuthResp, Nothing )
AuthOtp acc -> AuthOtp token ->
let let
sf = sf =
{ rememberMe = model.rememberMe { rememberMe = model.rememberMe
, token = Maybe.withDefault "" acc.token , token = token
, otp = model.otp , otp = model.otp
} }
in in
@ -53,7 +53,7 @@ update ( referrer, oauth ) flags msg model =
AuthResp (Ok lr) -> AuthResp (Ok lr) ->
let let
gotoRef = gotoRef =
Maybe.withDefault HomePage referrer |> Page.goto Maybe.withDefault HomePage loginData.referrer |> Page.goto
in in
if lr.success && not lr.requireSecondFactor then if lr.success && not lr.requireSecondFactor then
( { model | formState = AuthSuccess lr, password = "" } ( { model | formState = AuthSuccess lr, password = "" }
@ -62,7 +62,11 @@ update ( referrer, oauth ) flags msg model =
) )
else if lr.success && lr.requireSecondFactor then else if lr.success && lr.requireSecondFactor then
( { model | formState = FormInitial, authStep = StepOtp lr, password = "" } ( { model
| formState = FormInitial
, authStep = StepOtp <| Maybe.withDefault "" lr.token
, password = ""
}
, Cmd.none , Cmd.none
, Nothing , Nothing
) )
@ -77,11 +81,22 @@ update ( referrer, oauth ) flags msg model =
let let
empty = empty =
Api.Model.AuthResult.empty Api.Model.AuthResult.empty
session =
Maybe.withDefault "" loginData.session
in in
( { model | password = "", formState = HttpError err } -- A value of 2 indicates that TOTP is required
, Ports.removeAccount () if loginData.openid == 2 then
, Just empty ( { model | formState = FormInitial, authStep = StepOtp session, password = "" }
) , Cmd.none
, Nothing
)
else
( { model | password = "", formState = HttpError err }
, Ports.removeAccount ()
, Just empty
)
setAccount : AuthResult -> Cmd msg setAccount : AuthResult -> Cmd msg

View File

@ -104,11 +104,11 @@ openIdLinks texts flags =
] ]
otpForm : Texts -> Flags -> Model -> AuthResult -> Html Msg otpForm : Texts -> Flags -> Model -> String -> Html Msg
otpForm texts flags model acc = otpForm texts flags model token =
Html.form Html.form
[ action "#" [ action "#"
, onSubmit (AuthOtp acc) , onSubmit (AuthOtp token)
, autocomplete False , autocomplete False
] ]
[ div [ class "flex flex-col mt-6" ] [ div [ class "flex flex-col mt-6" ]

View File

@ -97,7 +97,7 @@ update flags msg model =
cmd = cmd =
if r.success then if r.success then
Page.goto (LoginPage ( Nothing, False )) Page.goto (LoginPage Page.emptyLoginData)
else else
Cmd.none Cmd.none

View File

@ -232,7 +232,7 @@ viewContent texts flags _ model =
[ text texts.alreadySignedUp [ text texts.alreadySignedUp
] ]
, a , a
[ Page.href (LoginPage ( Nothing, False )) [ Page.href (LoginPage Page.emptyLoginData)
, class ("ml-2" ++ S.link) , class ("ml-2" ++ S.link)
] ]
[ i [ class "fa fa-user-plus mr-1" ] [] [ i [ class "fa fa-user-plus mr-1" ] []