diff --git a/server/internal/api/oauth.go b/server/internal/api/oauth.go index 2764ed6a..d056abd5 100644 --- a/server/internal/api/oauth.go +++ b/server/internal/api/oauth.go @@ -32,7 +32,13 @@ func newOAuthHandler(service *oauth.Service, pathPrefix string, trustProxy, prov if pathPrefix == "" { pathPrefix = "/" } - return &oauthHandler{service: service, pathPrefix: pathPrefix, trustProxy: trustProxy, providerOAuth: providerOAuth} + + return &oauthHandler{ + service: service, + pathPrefix: pathPrefix, + trustProxy: trustProxy, + providerOAuth: providerOAuth, + } } func (api *ApiManagerCtx) OAuthConfig(w http.ResponseWriter, r *http.Request) error { @@ -48,17 +54,21 @@ func (api *ApiManagerCtx) OAuthLogin(w http.ResponseWriter, r *http.Request) err if !api.oauth.providerOAuth || !api.oauth.service.Enabled() { return utils.HttpNotFound() } + if !api.sessions.CookieEnabled() { return utils.HttpError(http.StatusServiceUnavailable, "OAuth requires session cookies to be enabled") } + callbackURL, err := api.oauth.callbackURL(r) if err != nil { return utils.HttpInternalServerError("unable to determine OAuth callback URL").WithInternalErr(err) } + location, err := api.oauth.service.Start(r.Context(), callbackURL) if err != nil { return oauthServiceError(err, true) } + http.Redirect(w, r, location, http.StatusFound) return nil } @@ -67,14 +77,17 @@ func (api *ApiManagerCtx) OAuthCallback(w http.ResponseWriter, r *http.Request) if !api.oauth.providerOAuth || !api.oauth.service.Enabled() { return utils.HttpNotFound() } + if providerError := r.URL.Query().Get("error"); providerError != "" { return utils.HttpUnauthorized("OAuth authorization was declined").WithInternalMsg(providerError) } + _, token, err := api.oauth.service.Complete(r.Context(), r.URL.Query().Get("state"), r.URL.Query().Get("code")) if err != nil { return oauthServiceError(err, false) } api.sessions.CookieSetToken(w, token) + http.Redirect(w, r, api.oauth.successRedirect(), http.StatusSeeOther) return nil } @@ -83,21 +96,27 @@ func oauthServiceError(err error, start bool) error { if errors.Is(err, types.ErrSessionAlreadyConnected) { return utils.HttpUnprocessableEntity("session already connected") } + if errors.Is(err, types.ErrSessionLoginsLocked) { return utils.HttpForbidden("logins are locked").WithInternalErr(err) } + if start && strings.HasPrefix(err.Error(), "issuer discovery:") { return utils.HttpError(http.StatusServiceUnavailable, "OAuth issuer discovery failed").WithInternalErr(err) } + if start { return utils.HttpError(http.StatusServiceUnavailable, err.Error()).WithInternalErr(err) } + if strings.Contains(err.Error(), "missing state or code") || strings.Contains(err.Error(), "state is invalid") { return utils.HttpBadRequest(err.Error()).WithInternalErr(err) } + if strings.HasPrefix(err.Error(), "issuer discovery:") || strings.Contains(err.Error(), "not fully configured") { return utils.HttpError(http.StatusServiceUnavailable, "OAuth issuer discovery failed").WithInternalErr(err) } + return utils.HttpUnauthorized("OAuth authorization failed").WithInternalErr(err) } @@ -106,6 +125,7 @@ func (handler *oauthHandler) callbackURL(r *http.Request) (string, error) { if r.TLS != nil { scheme = "https" } + host := r.Host if handler.trustProxy { if value := forwardedHeaderValue(r.Header.Get("X-Forwarded-Proto")); value != "" { @@ -115,22 +135,32 @@ func (handler *oauthHandler) callbackURL(r *http.Request) (string, error) { host = value } } + if host == "" || (scheme != "http" && scheme != "https") { return "", errors.New("request has no valid public URL") } - callback := (&url.URL{Scheme: scheme, Host: host, Path: path.Join(handler.pathPrefix, "/api/oauth/callback")}).String() + + callback := (&url.URL{ + Scheme: scheme, + Host: host, + Path: path.Join(handler.pathPrefix, "/api/oauth/callback"), + }).String() if parsed, err := url.Parse(callback); err != nil || parsed.Host == "" { return "", errors.New("request has no valid public URL") } + return callback, nil } + func (handler *oauthHandler) successRedirect() string { redirect := handler.service.SuccessRedirect() if strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") { return path.Join(handler.pathPrefix, redirect) } + return handler.pathPrefix } + func forwardedHeaderValue(value string) string { return strings.TrimSpace(strings.Split(value, ",")[0]) } diff --git a/server/internal/api/router.go b/server/internal/api/router.go index 10520dfa..c2033c3f 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -43,7 +43,12 @@ func New( desktop: desktop, capture: capture, routers: make(map[string]func(types.Router)), - oauth: newOAuthHandler(oauth.NewService(memberConfig.OAuth, sessions), pathPrefix, serverConfig != nil && serverConfig.Proxy, memberConfig.Provider == "oauth"), + oauth: newOAuthHandler( + oauth.NewService(memberConfig.OAuth, sessions), + pathPrefix, + serverConfig != nil && serverConfig.Proxy, + memberConfig.Provider == "oauth", + ), } } diff --git a/server/internal/api/session.go b/server/internal/api/session.go index 6c5a7d24..f054688e 100644 --- a/server/internal/api/session.go +++ b/server/internal/api/session.go @@ -67,6 +67,7 @@ func (api *ApiManagerCtx) Logout(w http.ResponseWriter, r *http.Request) error { return utils.HttpInternalServerError().WithInternalErr(err) } } + if api.sessions.CookieEnabled() { api.sessions.CookieClearToken(w, r) } diff --git a/webpage/docs/configuration/authentication.md b/webpage/docs/configuration/authentication.md index d0db2813..f853f9f5 100644 --- a/webpage/docs/configuration/authentication.md +++ b/webpage/docs/configuration/authentication.md @@ -25,7 +25,7 @@ A member profile is a structure that describes the user and what the user is all | Field | Description | Type | |----------------------------|-------------|------| | | User's name as shown in the UI, must not be unique within the system (not used as an identifier). | string | -| `avatar` | Avatar URL shown in clients that support member avatars. OAuth logins synchronize this value from the configured user-info field. | string | +| | Avatar URL shown in clients that support member avatars. OAuth logins synchronize this value from the configured user-info field. | string | | | Whether the user can perform administrative tasks that include managing users, sessions, and settings. | boolean | | | Whether the user can log in to the system and use the HTTP API. | boolean | | | Whether the user can connect to the room using the WebSocket API (needs to be enabled). | boolean |