mattermost-community-enterp.../channels/app/oauth.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

1233 lines
48 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
b64 "encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/i18n"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/v8/einterfaces"
)
const (
OAuthCookieMaxAgeSeconds = 30 * 60 // 30 minutes
CookieOAuth = "MMOAUTH"
OpenIDScope = "openid"
)
func (a *App) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
// Public method for plugin API - always generates secrets for backward compatibility
return a.CreateOAuthAppInternal(app, true)
}
// CreateOAuthAppInternal creates an OAuth app with optional secret generation.
// If generateSecret is true and ClientSecret is empty, a secret will be auto-generated.
// If generateSecret is false, the ClientSecret is left as-is (empty for public clients).
func (a *App) CreateOAuthAppInternal(app *model.OAuthApp, generateSecret bool) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("CreateOAuthApp", "api.oauth.register_oauth_app.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
// Generate a client secret if requested and not already set
if generateSecret {
app.ClientSecret = model.NewId()
}
oauthApp, err := a.Srv().Store().OAuth().SaveApp(app)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
a.Log().Error("Error saving OAuth app", mlog.Err(err), mlog.String("name", app.Name))
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateOAuthApp", "app.oauth.save_app.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateOAuthApp", "app.oauth.save_app.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return oauthApp, nil
}
func (a *App) GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, err := a.Srv().Store().OAuth().GetApp(appID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetOAuthApp", "app.oauth.get_app.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetOAuthApp", "app.oauth.get_app.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return oauthApp, nil
}
func (a *App) UpdateOAuthApp(oldApp, updatedApp *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("UpdateOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
updatedApp.Id = oldApp.Id
updatedApp.CreatorId = oldApp.CreatorId
updatedApp.CreateAt = oldApp.CreateAt
updatedApp.ClientSecret = oldApp.ClientSecret
updatedApp.IsDynamicallyRegistered = oldApp.IsDynamicallyRegistered
oauthApp, err := a.Srv().Store().OAuth().UpdateApp(updatedApp)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateOAuthApp", "app.oauth.update_app.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateOAuthApp", "app.oauth.update_app.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return oauthApp, nil
}
func (a *App) DeleteOAuthApp(rctx request.CTX, appID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return model.NewAppError("DeleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if err := a.Srv().Store().OAuth().DeleteApp(appID); err != nil {
return model.NewAppError("DeleteOAuthApp", "app.oauth.delete_app.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().InvalidateAllCaches(); err != nil {
rctx.Logger().Warn("error in invalidating cache", mlog.Err(err))
}
return nil
}
func (a *App) GetOAuthApps(page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthApps", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApps, err := a.Srv().Store().OAuth().GetApps(page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOAuthApps", "app.oauth.get_apps.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return oauthApps, nil
}
func (a *App) GetOAuthAppsByCreator(userID string, page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAppsByUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApps, err := a.Srv().Store().OAuth().GetAppByUser(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOAuthAppsByCreator", "app.oauth.get_app_by_user.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return oauthApps, nil
}
func (a *App) GetOAuthImplicitRedirect(rctx request.CTX, userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
session, err := a.GetOAuthAccessTokenForImplicitFlow(rctx, userID, authRequest)
if err != nil {
return "", err
}
values := &url.Values{}
values.Add("access_token", session.Token)
values.Add("token_type", "bearer")
values.Add("expires_in", strconv.FormatInt((session.ExpiresAt-model.GetMillis())/1000, 10))
values.Add("scope", authRequest.Scope)
values.Add("state", authRequest.State)
return fmt.Sprintf("%s#%s", authRequest.RedirectURI, values.Encode()), nil
}
func (a *App) GetOAuthCodeRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
authData := &model.AuthData{
UserId: userID,
ClientId: authRequest.ClientId,
CreateAt: model.GetMillis(),
RedirectUri: authRequest.RedirectURI,
State: authRequest.State,
Scope: authRequest.Scope,
CodeChallenge: authRequest.CodeChallenge,
CodeChallengeMethod: authRequest.CodeChallengeMethod,
Resource: authRequest.Resource,
}
authData.Code = model.NewId() + model.NewId()
// parse authRequest.RedirectURI to handle query parameters see: https://mattermost.atlassian.net/browse/MM-46216
uri, err := url.Parse(authRequest.RedirectURI)
if err != nil {
return authRequest.RedirectURI + "?error=redirect_uri_parse_error&state=" + authRequest.State, nil
}
queryParams := uri.Query()
if _, err := a.Srv().Store().OAuth().SaveAuthData(authData); err != nil {
queryParams.Set("error", "server_error")
queryParams.Set("state", authRequest.State)
uri.RawQuery = queryParams.Encode()
return uri.String(), nil
}
queryParams.Set("code", authData.Code)
queryParams.Set("state", authData.State)
uri.RawQuery = queryParams.Encode()
return uri.String(), nil
}
func (a *App) AllowOAuthAppAccessToUser(rctx request.CTX, userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if authRequest.Scope == "" {
authRequest.Scope = model.DefaultScope
}
oauthApp, nErr := a.Srv().Store().OAuth().GetApp(authRequest.ClientId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return "", model.NewAppError("AllowOAuthAppAccessToUser", "app.oauth.get_app.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return "", model.NewAppError("AllowOAuthAppAccessToUser", "app.oauth.get_app.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if !oauthApp.IsValidRedirectURL(authRequest.RedirectURI) {
return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest)
}
// Validate PKCE requirements for public clients
if oauthApp.IsPublicClient() && authRequest.ResponseType == model.AuthCodeResponseType && authRequest.CodeChallenge == "" {
return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.pkce_required_public.app_error", nil, "", http.StatusBadRequest)
}
var redirectURI string
var err *model.AppError
switch authRequest.ResponseType {
case model.AuthCodeResponseType:
redirectURI, err = a.GetOAuthCodeRedirect(userID, authRequest)
case model.ImplicitResponseType:
redirectURI, err = a.GetOAuthImplicitRedirect(rctx, userID, authRequest)
default:
return authRequest.RedirectURI + "?error=unsupported_response_type&state=" + authRequest.State, nil
}
if err != nil {
rctx.Logger().Warn("error getting oauth redirect uri", mlog.Err(err))
return authRequest.RedirectURI + "?error=server_error&state=" + authRequest.State, nil
}
// This saves the OAuth2 app as authorized
authorizedApp := model.Preference{
UserId: userID,
Category: model.PreferenceCategoryAuthorizedOAuthApp,
Name: authRequest.ClientId,
Value: authRequest.Scope,
}
if nErr := a.Srv().Store().Preference().Save(model.Preferences{authorizedApp}); nErr != nil {
rctx.Logger().Warn("error saving store preference", mlog.Err(nErr))
return authRequest.RedirectURI + "?error=server_error&state=" + authRequest.State, nil
}
return redirectURI, nil
}
func (a *App) GetOAuthAccessTokenForImplicitFlow(rctx request.CTX, userID string, authRequest *model.AuthorizeRequest) (*model.Session, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, err := a.GetOAuthApp(authRequest.ClientId)
if err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusNotFound).Wrap(err)
}
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
session, err := a.newSession(rctx, oauthApp, user)
if err != nil {
return nil, err
}
accessData := &model.AccessData{ClientId: authRequest.ClientId, UserId: user.Id, Token: session.Token, RefreshToken: "", RedirectUri: authRequest.RedirectURI, ExpiresAt: session.ExpiresAt, Scope: authRequest.Scope}
if _, err := a.Srv().Store().OAuth().SaveAccessData(accessData); err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return session, nil
}
func (a *App) GetOAuthAccessTokenForCodeFlow(rctx request.CTX, clientId, grantType, redirectURI, code, secret, refreshToken, codeVerifier, resource string) (*model.AccessResponse, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, nErr := a.Srv().Store().OAuth().GetApp(clientId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
}
if err := a.validateOAuthClient(oauthApp, grantType, secret, codeVerifier); err != nil {
return nil, err
}
if grantType == model.AccessTokenGrantType {
return a.handleAuthorizationCodeGrant(rctx, oauthApp, redirectURI, code, codeVerifier, clientId, resource)
}
return a.handleRefreshTokenGrant(rctx, oauthApp, refreshToken, resource)
}
func (a *App) validateOAuthClient(oauthApp *model.OAuthApp, grantType, secret, codeVerifier string) *model.AppError {
return oauthApp.ValidateForGrantType(grantType, secret, codeVerifier)
}
func (a *App) validatePKCE(oauthApp *model.OAuthApp, authData *model.AuthData, codeVerifier string) *model.AppError {
return authData.ValidatePKCEForClientType(oauthApp.IsPublicClient(), codeVerifier)
}
func (a *App) handleAuthorizationCodeGrant(rctx request.CTX, oauthApp *model.OAuthApp, redirectURI, code, codeVerifier, clientId, resource string) (*model.AccessResponse, *model.AppError) {
authData, nErr := a.Srv().Store().OAuth().GetAuthData(code)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
if authData.IsExpired() {
if nErr = a.Srv().Store().OAuth().RemoveAuthData(authData.Code); nErr != nil {
rctx.Logger().Warn("unable to remove auth data", mlog.Err(nErr))
}
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusForbidden)
}
if authData.RedirectUri != redirectURI {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.redirect_uri.app_error", nil, "", http.StatusBadRequest)
}
if err := a.validatePKCE(oauthApp, authData, codeVerifier); err != nil {
return nil, err
}
user, nErr := a.Srv().Store().User().Get(context.Background(), authData.UserId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
}
if user.DeleteAt != 0 {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusForbidden)
}
defer func() {
if nErr = a.Srv().Store().OAuth().RemoveAuthData(authData.Code); nErr != nil {
rctx.Logger().Warn("unable to remove auth data", mlog.Err(nErr))
}
}()
var audience string
if resource != "" {
// Validate the resource parameter per RFC 8707
if err := model.ValidateResourceParameter(resource, clientId, "handleAuthorizationCodeGrant"); err != nil {
return nil, err
}
// Validate resource parameter consistency between authorization and token requests
if authData.Resource != "" && resource != authData.Resource {
return nil, model.NewAppError("handleAuthorizationCodeGrant", "api.oauth.get_access_token.resource_mismatch.app_error", nil, "client_id="+clientId, http.StatusBadRequest)
}
audience = resource
} else if authData.Resource != "" {
audience = authData.Resource // Use resource from authorization request
}
return a.generateAccessTokenResponse(rctx, oauthApp, user, clientId, redirectURI, authData.Scope, audience)
}
func (a *App) handleRefreshTokenGrant(rctx request.CTX, oauthApp *model.OAuthApp, refreshToken, resource string) (*model.AccessResponse, *model.AppError) {
// Validate that this client can use refresh token grant type
if err := oauthApp.ValidateForGrantType(model.RefreshTokenGrantType, oauthApp.ClientSecret, ""); err != nil {
return nil, err
}
accessData, nErr := a.Srv().Store().OAuth().GetAccessDataByRefreshToken(refreshToken)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
}
user, nErr := a.Srv().Store().User().Get(context.Background(), accessData.UserId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
}
audience := accessData.Audience // Default to existing audience
if resource != "" {
// Validate the resource parameter per RFC 8707
if err := model.ValidateResourceParameter(resource, oauthApp.Id, "handleRefreshTokenGrant"); err != nil {
return nil, err
}
// For refresh tokens, resource parameter must match the original audience
if accessData.Audience != "" && resource != accessData.Audience {
return nil, model.NewAppError("handleRefreshTokenGrant", "api.oauth.get_access_token.resource_mismatch.app_error", nil, "client_id="+oauthApp.Id, http.StatusBadRequest)
}
audience = resource
}
return a.newSessionUpdateToken(rctx, oauthApp, accessData, user, audience)
}
func (a *App) generateAccessTokenResponse(rctx request.CTX, oauthApp *model.OAuthApp, user *model.User, clientId, redirectURI, scope, audience string) (*model.AccessResponse, *model.AppError) {
accessData, nErr := a.Srv().Store().OAuth().GetPreviousAccessData(user.Id, clientId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
if accessData != nil {
return a.handleExistingAccessData(rctx, oauthApp, accessData, user, audience)
}
return a.createNewAccessData(rctx, oauthApp, user, clientId, redirectURI, scope, audience)
}
func (a *App) handleExistingAccessData(rctx request.CTX, oauthApp *model.OAuthApp, accessData *model.AccessData, user *model.User, audience string) (*model.AccessResponse, *model.AppError) {
if accessData.IsExpired() {
return a.newSessionUpdateToken(rctx, oauthApp, accessData, user, audience)
}
refreshToken := accessData.RefreshToken
if oauthApp.IsPublicClient() {
refreshToken = ""
}
audienceStr := accessData.Audience
return &model.AccessResponse{
AccessToken: accessData.Token,
TokenType: model.AccessTokenType,
RefreshToken: refreshToken,
ExpiresInSeconds: int32((accessData.ExpiresAt - model.GetMillis()) / 1000),
Audience: audienceStr,
}, nil
}
func (a *App) createNewAccessData(rctx request.CTX, oauthApp *model.OAuthApp, user *model.User, clientId, redirectURI, scope string, audience string) (*model.AccessResponse, *model.AppError) {
session, err := a.newSession(rctx, oauthApp, user)
if err != nil {
return nil, err
}
refreshToken := ""
if !oauthApp.IsPublicClient() {
refreshToken = model.NewId()
}
accessData := &model.AccessData{
ClientId: clientId,
UserId: user.Id,
Token: session.Token,
RefreshToken: refreshToken,
RedirectUri: redirectURI,
ExpiresAt: session.ExpiresAt,
Scope: scope,
Audience: audience,
}
if _, nErr := a.Srv().Store().OAuth().SaveAccessData(accessData); nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
refreshTokenResponse := accessData.RefreshToken
if oauthApp.IsPublicClient() {
refreshTokenResponse = ""
}
audienceStr := audience
return &model.AccessResponse{
AccessToken: session.Token,
TokenType: model.AccessTokenType,
RefreshToken: refreshTokenResponse,
ExpiresInSeconds: int32(*a.Config().ServiceSettings.SessionLengthSSOInHours * 60 * 60),
Audience: audienceStr,
}, nil
}
func (a *App) newSession(rctx request.CTX, app *model.OAuthApp, user *model.User) (*model.Session, *model.AppError) {
if err := a.limitNumberOfSessions(rctx, user.Id); err != nil {
return nil, model.NewAppError("newSession", "api.oauth.get_access_token.internal_session.app_error", nil,
"", http.StatusInternalServerError).Wrap(err)
}
// Set new token an session
session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true}
session.GenerateCSRF()
a.ch.srv.platform.SetSessionExpireInHours(session, *a.Config().ServiceSettings.SessionLengthSSOInHours)
session.AddProp(model.SessionPropPlatform, app.Name)
session.AddProp(model.SessionPropOAuthAppID, app.Id)
session.AddProp(model.SessionPropMattermostAppID, app.MattermostAppID)
session.AddProp(model.SessionPropOs, "OAuth2")
session.AddProp(model.SessionPropBrowser, "OAuth2")
session, err := a.Srv().Store().Session().Save(rctx, session)
if err != nil {
return nil, model.NewAppError("newSession", "api.oauth.get_access_token.internal_session.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.ch.srv.platform.AddSessionToCache(session); err != nil {
rctx.Logger().Warn("Failed to add session to cache", mlog.Err(err))
}
return session, nil
}
func (a *App) newSessionUpdateToken(rctx request.CTX, app *model.OAuthApp, accessData *model.AccessData, user *model.User, audience string) (*model.AccessResponse, *model.AppError) {
// Remove the previous session
if err := a.Srv().Store().Session().Remove(accessData.Token); err != nil {
rctx.Logger().Warn("error removing access data token from session", mlog.Err(err))
}
session, err := a.newSession(rctx, app, user)
if err != nil {
return nil, err
}
accessData.Token = session.Token
// Generate refresh token only for confidential clients
if !app.IsPublicClient() {
accessData.RefreshToken = model.NewId()
} else {
accessData.RefreshToken = ""
}
accessData.ExpiresAt = session.ExpiresAt
// Update audience if provided (for refresh token with resource parameter)
if audience != "" {
accessData.Audience = audience
}
if _, err := a.Srv().Store().OAuth().UpdateAccessData(accessData); err != nil {
return nil, model.NewAppError("newSessionUpdateToken", "web.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
audienceStr := accessData.Audience
accessRsp := &model.AccessResponse{
AccessToken: session.Token,
RefreshToken: accessData.RefreshToken,
TokenType: model.AccessTokenType,
ExpiresInSeconds: int32(*a.Config().ServiceSettings.SessionLengthSSOInHours * 60 * 60),
Audience: audienceStr,
}
return accessRsp, nil
}
func (a *App) GetOAuthLoginEndpoint(rctx request.CTX, w http.ResponseWriter, r *http.Request, service, action, redirectTo, loginHint string, isMobile bool, desktopToken string, inviteToken string, inviteId string) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = action
if inviteToken != "" {
stateProps["invite_token"] = inviteToken
} else if inviteId != "" {
stateProps["invite_id"] = inviteId
}
if redirectTo != "" {
stateProps["redirect_to"] = redirectTo
}
if desktopToken != "" {
stateProps["desktop_token"] = desktopToken
}
stateProps[model.UserAuthServiceIsMobile] = strconv.FormatBool(isMobile)
authURL, err := a.GetAuthorizationCode(rctx, w, r, service, stateProps, loginHint)
if err != nil {
return "", err
}
return authURL, nil
}
func (a *App) GetOAuthSignupEndpoint(rctx request.CTX, w http.ResponseWriter, r *http.Request, service, desktopToken string, inviteToken string, inviteId string) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = model.OAuthActionSignup
if inviteToken != "" {
stateProps["invite_token"] = inviteToken
} else if inviteId != "" {
stateProps["invite_id"] = inviteId
}
if desktopToken != "" {
stateProps["desktop_token"] = desktopToken
}
authURL, err := a.GetAuthorizationCode(rctx, w, r, service, stateProps, "")
if err != nil {
return "", err
}
return authURL, nil
}
func (a *App) GetAuthorizedAppsForUser(userID string, page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetAuthorizedAppsForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
apps, err := a.Srv().Store().OAuth().GetAuthorizedApps(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetAuthorizedAppsForUser", "app.oauth.get_apps.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for k, a := range apps {
a.Sanitize()
apps[k] = a
}
return apps, nil
}
func (a *App) DeauthorizeOAuthAppForUser(rctx request.CTX, userID, appID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return model.NewAppError("DeauthorizeOAuthAppForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
// Revoke app sessions
accessData, err := a.Srv().Store().OAuth().GetAccessDataByUserForApp(userID, appID)
if err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.oauth.get_access_data_by_user_for_app.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, ad := range accessData {
if err := a.RevokeAccessToken(rctx, ad.Token); err != nil {
return err
}
if err := a.Srv().Store().OAuth().RemoveAccessData(ad.Token); err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.oauth.remove_access_data.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if err := a.Srv().Store().OAuth().RemoveAuthDataByClientId(appID, userID); err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.oauth.remove_auth_data_by_client_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Deauthorize the app
if err := a.Srv().Store().Preference().Delete(userID, model.PreferenceCategoryAuthorizedOAuthApp, appID); err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.preference.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("RegenerateOAuthAppSecret", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
app.ClientSecret = model.NewId()
if _, err := a.Srv().Store().OAuth().UpdateApp(app); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("RegenerateOAuthAppSecret", "app.oauth.update_app.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("RegenerateOAuthAppSecret", "app.oauth.update_app.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return app, nil
}
func (a *App) RevokeAccessToken(rctx request.CTX, token string) *model.AppError {
if err := a.ch.srv.platform.RevokeAccessToken(rctx, token); err != nil {
switch {
case errors.Is(err, platform.GetTokenError):
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.Is(err, platform.DeleteTokenError):
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case errors.Is(err, platform.DeleteSessionError):
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) CompleteOAuth(rctx request.CTX, service string, body io.ReadCloser, props map[string]string, tokenUser *model.User) (*model.User, *model.AppError) {
defer body.Close()
action := props["action"]
// Extract invite token or ID from props so we can add the user to the team if needed
inviteToken := props["invite_token"]
inviteId := props["invite_id"]
switch action {
case model.OAuthActionSignup:
return a.CreateOAuthUser(rctx, service, body, inviteToken, inviteId, tokenUser)
case model.OAuthActionLogin:
return a.LoginByOAuth(rctx, service, body, inviteToken, inviteId, tokenUser)
case model.OAuthActionEmailToSSO:
return a.CompleteSwitchWithOAuth(rctx, service, body, props["email"], tokenUser)
case model.OAuthActionSSOToEmail:
return a.LoginByOAuth(rctx, service, body, inviteToken, inviteId, tokenUser)
default:
return a.LoginByOAuth(rctx, service, body, inviteToken, inviteId, tokenUser)
}
}
func (a *App) getSSOProvider(service string) (einterfaces.OAuthProvider, *model.AppError) {
sso := a.Config().GetSSOService(service)
if sso == nil || !*sso.Enable {
return nil, model.NewAppError("getSSOProvider", "api.user.authorize_oauth_user.unsupported.app_error", nil, "service="+service, http.StatusNotImplemented)
}
providerType := service
if strings.Contains(*sso.Scope, OpenIDScope) {
providerType = model.ServiceOpenid
}
provider := einterfaces.GetOAuthProvider(providerType)
if provider == nil {
return nil, model.NewAppError("getSSOProvider", "api.user.login_by_oauth.not_available.app_error",
map[string]any{"Service": strings.Title(service)}, "", http.StatusNotImplemented)
}
return provider, nil
}
func (a *App) LoginByOAuth(rctx request.CTX, service string, userData io.Reader, inviteToken string, inviteId string, tokenUser *model.User) (*model.User, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, e
}
buf := bytes.Buffer{}
if _, err := buf.ReadFrom(userData); err != nil {
return nil, model.NewAppError("LoginByOAuth2", "api.user.login_by_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest)
}
authUser, err1 := provider.GetUserFromJSON(rctx, bytes.NewReader(buf.Bytes()), tokenUser)
if err1 != nil {
return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest).Wrap(err1)
}
if *authUser.AuthData == "" {
return nil, model.NewAppError("LoginByOAuth3", "api.user.login_by_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest)
}
user, err := a.GetUserByAuth(model.NewPointer(*authUser.AuthData), service)
if err != nil {
if err.Id == MissingAuthAccountError {
user, err = a.CreateOAuthUser(rctx, service, bytes.NewReader(buf.Bytes()), inviteToken, inviteId, tokenUser)
} else {
return nil, err
}
} else {
// OAuth doesn't run through CheckUserPreflightAuthenticationCriteria, so prevent bot login
// here manually. Technically, the auth data above will fail to match a bot in the first
// place, but explicit is always better.
if user.IsBot {
return nil, model.NewAppError("loginByOAuth", "api.user.login_by_oauth.bot_login_forbidden.app_error", nil, "", http.StatusForbidden)
}
if err = a.UpdateOAuthUserAttrs(rctx, bytes.NewReader(buf.Bytes()), user, provider, service, tokenUser); err != nil {
return nil, err
}
if err = a.AddUserToTeamByInviteIfNeeded(rctx, user, inviteToken, inviteId); err != nil {
rctx.Logger().Warn("Failed to add user to team", mlog.Err(err))
}
}
if err != nil {
return nil, err
}
return user, nil
}
func (a *App) CompleteSwitchWithOAuth(rctx request.CTX, service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, e
}
if email == "" {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.blank_email.app_error", nil, "", http.StatusBadRequest)
}
ssoUser, err1 := provider.GetUserFromJSON(rctx, userData, tokenUser)
if err1 != nil {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest).Wrap(err1)
}
if *ssoUser.AuthData == "" {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest)
}
user, nErr := a.Srv().Store().User().GetByEmail(email)
if nErr != nil {
return nil, model.NewAppError("CompleteSwitchWithOAuth", MissingAccountError, nil, "", http.StatusInternalServerError).Wrap(nErr)
}
if err := a.RevokeAllSessions(rctx, user.Id); err != nil {
return nil, err
}
if _, nErr := a.Srv().Store().User().UpdateAuthData(user.Id, service, ssoUser.AuthData, ssoUser.Email, true); nErr != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("importUser", "app.user.update_auth_data.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("importUser", "app.user.update_auth_data.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, a.GetSiteURL()); err != nil {
rctx.Logger().Error("error sending signin change email", mlog.Err(err))
}
})
return user, nil
}
func (a *App) CreateOAuthStateToken(extra string) (*model.Token, *model.AppError) {
token := model.NewToken(model.TokenTypeOAuth, extra)
if err := a.Srv().Store().Token().Save(token); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateOAuthStateToken", "app.recover.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return token, nil
}
func (a *App) GetOAuthStateToken(token string) (*model.Token, *model.AppError) {
mToken, err := a.Srv().Store().Token().GetByToken(token)
if err != nil {
return nil, model.NewAppError("GetOAuthStateToken", "api.oauth.invalid_state_token.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if mToken.Type != model.TokenTypeOAuth {
return nil, model.NewAppError("GetOAuthStateToken", "api.oauth.invalid_state_token.app_error", nil, "", http.StatusBadRequest)
}
return mToken, nil
}
func (a *App) GetAuthorizationCode(rctx request.CTX, w http.ResponseWriter, r *http.Request, service string, props map[string]string, loginHint string) (string, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return "", e
}
sso, e2 := provider.GetSSOSettings(rctx, a.Config(), service)
if e2 != nil {
return "", model.NewAppError("GetAuthorizationCode.GetSSOSettings", "api.user.get_authorization_code.endpoint.app_error", nil, "", http.StatusNotImplemented).Wrap(e2)
}
secure := false
if GetProtocol(r) == "https" {
secure = true
}
cookieValue := model.NewId()
subpath, _ := utils.GetSubpathFromConfig(a.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(OAuthCookieMaxAgeSeconds), 0)
oauthCookie := &http.Cookie{
Name: CookieOAuth,
Value: cookieValue,
Path: subpath,
MaxAge: OAuthCookieMaxAgeSeconds,
Expires: expiresAt,
HttpOnly: true,
Secure: secure,
}
http.SetCookie(w, oauthCookie)
clientId := *sso.Id
endpoint := *sso.AuthEndpoint
scope := *sso.Scope
tokenExtra := generateOAuthStateTokenExtra(props["email"], props["action"], cookieValue)
stateToken, err := a.CreateOAuthStateToken(tokenExtra)
if err != nil {
return "", err
}
props["token"] = stateToken.Token
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJSON(props)))
siteURL := a.GetSiteURL()
if strings.TrimSpace(siteURL) == "" {
siteURL = GetProtocol(r) + "://" + r.Host
}
redirectURI := siteURL + "/signup/" + service + "/complete"
authURL := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectURI) + "&state=" + url.QueryEscape(state)
if scope != "" {
authURL += "&scope=" + utils.URLEncode(scope)
}
if loginHint != "" {
authURL += "&login_hint=" + utils.URLEncode(loginHint)
}
return authURL, nil
}
func (a *App) AuthorizeOAuthUser(rctx request.CTX, w http.ResponseWriter, r *http.Request, service, code, state, redirectURI string) (io.ReadCloser, map[string]string, *model.User, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, nil, nil, e
}
sso, e2 := provider.GetSSOSettings(rctx, a.Config(), service)
if e2 != nil {
return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser.GetSSOSettings", "api.user.get_authorization_code.endpoint.app_error", nil, "", http.StatusNotImplemented).Wrap(e2)
}
b, strErr := b64.StdEncoding.DecodeString(state)
if strErr != nil {
return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest).Wrap(strErr)
}
stateStr := string(b)
stateProps := model.MapFromJSON(strings.NewReader(stateStr))
expectedToken, appErr := a.GetOAuthStateToken(stateProps["token"])
if appErr != nil {
return nil, stateProps, nil, appErr
}
stateEmail := stateProps["email"]
stateAction := stateProps["action"]
if stateAction == model.OAuthActionEmailToSSO && stateEmail == "" {
err := errors.New("No email provided in state when trying to switch from email to SSO")
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
cookie, cookieErr := r.Cookie(CookieOAuth)
if cookieErr != nil {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest).Wrap(cookieErr)
}
tokenEmail, tokenAction, tokenCookie, parseErr := parseOAuthStateTokenExtra(expectedToken.Extra)
if parseErr != nil {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest).Wrap(parseErr)
}
if tokenEmail != stateEmail || tokenAction != stateAction || tokenCookie != cookie.Value {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest).Wrap(errors.New("invalid state token"))
}
appErr = a.DeleteToken(expectedToken)
if appErr != nil {
rctx.Logger().Warn("error deleting token", mlog.Err(appErr))
}
subpath, _ := utils.GetSubpathFromConfig(a.Config())
httpCookie := &http.Cookie{
Name: CookieOAuth,
Value: "",
Path: subpath,
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(w, httpCookie)
p := url.Values{}
p.Set("client_id", *sso.Id)
p.Set("client_secret", *sso.Secret)
p.Set("code", code)
p.Set("grant_type", model.AccessTokenGrantType)
p.Set("redirect_uri", redirectURI)
req, requestErr := http.NewRequest("POST", *sso.TokenEndpoint, strings.NewReader(p.Encode()))
if requestErr != nil {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, "", http.StatusInternalServerError).Wrap(requestErr)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
resp, err := a.HTTPService().MakeClient(true).Do(req)
if err != nil {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer resp.Body.Close()
var buf bytes.Buffer
tee := io.TeeReader(resp.Body, &buf)
var ar *model.AccessResponse
err = json.NewDecoder(tee).Decode(&ar)
if err != nil || resp.StatusCode != http.StatusOK {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_response.app_error", nil, fmt.Sprintf("response_body=%s, status_code=%d, error=%v", buf.String(), resp.StatusCode, err), http.StatusInternalServerError).Wrap(err)
}
if strings.ToLower(ar.TokenType) != model.AccessTokenType {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_token.app_error", nil, "token_type="+ar.TokenType+", response_body="+buf.String(), http.StatusInternalServerError)
}
if ar.AccessToken == "" {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.missing.app_error", nil, "response_body="+buf.String(), http.StatusInternalServerError)
}
p = url.Values{}
p.Set("access_token", ar.AccessToken)
var userFromToken *model.User
if ar.IdToken != "" {
userFromToken, err = provider.GetUserFromIdToken(rctx, ar.IdToken)
if err != nil {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
req, requestErr = http.NewRequest("GET", *sso.UserAPIEndpoint, strings.NewReader(""))
if requestErr != nil {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]any{"Service": service}, "", http.StatusInternalServerError).Wrap(requestErr)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+ar.AccessToken)
resp, err = a.HTTPService().MakeClient(true).Do(req)
if err != nil {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]any{"Service": service}, "", http.StatusInternalServerError).Wrap(err)
} else if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
// Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil
bodyBytes, _ := io.ReadAll(resp.Body)
bodyString := string(bodyBytes)
rctx.Logger().Error("Error getting OAuth user", mlog.Int("response", resp.StatusCode), mlog.String("body_string", bodyString))
if service == model.ServiceGitlab && resp.StatusCode == http.StatusForbidden && strings.Contains(bodyString, "Terms of Service") {
url, err := url.Parse(*sso.UserAPIEndpoint)
if err != nil {
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(errors.Wrapf(err, "error parsing %s", *sso.UserAPIEndpoint))
}
// Return a nicer error when the user hasn't accepted GitLab's terms of service
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "oauth.gitlab.tos.error", map[string]any{"URL": url.Hostname()}, "", http.StatusBadRequest)
}
return nil, stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.response.app_error", nil, "response_body="+bodyString, http.StatusInternalServerError)
}
// Note that resp.Body is not closed here, so it must be closed by the caller
return resp.Body, stateProps, userFromToken, nil
}
func (a *App) SwitchEmailToOAuth(rctx request.CTX, w http.ResponseWriter, r *http.Request, email, password, code, service string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("emailToOAuth", "api.user.email_to_oauth.not_available.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if err = a.CheckPasswordAndAllCriteria(rctx, user.Id, password, code); err != nil {
return "", err
}
stateProps := map[string]string{}
stateProps["action"] = model.OAuthActionEmailToSSO
stateProps["email"] = email
if service == model.UserAuthServiceSaml {
samlToken, samlErr := a.CreateSamlRelayToken(model.TokenTypeSaml, email)
if samlErr != nil {
return "", samlErr
}
return a.GetSiteURL() + "/login/sso/saml?action=" + model.OAuthActionEmailToSSO + "&email_token=" + utils.URLEncode(samlToken.Token), nil
}
authURL, err := a.GetAuthorizationCode(rctx, w, r, service, stateProps, "")
if err != nil {
return "", err
}
return authURL, nil
}
func (a *App) SwitchOAuthToEmail(rctx request.CTX, email, password, requesterId string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("oauthToEmail", "api.user.oauth_to_email.not_available.app_error", nil, "", http.StatusForbidden)
}
if !*a.Config().EmailSettings.EnableSignUpWithEmail {
return "", model.NewAppError("SwitchOAuthToEmail", "api.user.auth_switch.not_available.email_signup_disabled.app_error", nil, "", http.StatusForbidden)
}
if !*a.Config().EmailSettings.EnableSignInWithEmail && !*a.Config().EmailSettings.EnableSignInWithUsername {
return "", model.NewAppError("SwitchOAuthToEmail", "api.user.auth_switch.not_available.login_disabled.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if user.Id != requesterId {
return "", model.NewAppError("SwitchOAuthToEmail", "api.user.oauth_to_email.context.app_error", nil, "", http.StatusForbidden)
}
if err := a.UpdatePassword(rctx, user, password); err != nil {
return "", err
}
T := i18n.GetUserTranslations(user.Locale)
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil {
rctx.Logger().Error("error sending signin change email", mlog.Err(err))
}
})
if err := a.RevokeAllSessions(rctx, requesterId); err != nil {
return "", err
}
return "/login?extra=signin_change", nil
}
func generateOAuthStateTokenExtra(email, action, cookie string) string {
return email + ":" + action + ":" + cookie
}
func (a *App) GetAuthorizationServerMetadata(rctx request.CTX) (*model.AuthorizationServerMetadata, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetAuthorizationServerMetadata", "api.oauth.authorization_server_metadata.disabled.app_error", nil, "", http.StatusNotImplemented)
}
siteURL := *a.Config().ServiceSettings.SiteURL
if siteURL == "" {
return nil, model.NewAppError("GetAuthorizationServerMetadata", "api.oauth.authorization_server_metadata.site_url_required.app_error", nil, "", http.StatusInternalServerError)
}
metadata, err := model.GetDefaultMetadata(siteURL)
if err != nil {
return nil, model.NewAppError("GetAuthorizationServerMetadata", "api.oauth.authorization_server_metadata.invalid_url.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if a.Config().ServiceSettings.EnableDynamicClientRegistration != nil && *a.Config().ServiceSettings.EnableDynamicClientRegistration {
metadata.RegistrationEndpoint, err = url.JoinPath(siteURL, model.OAuthAppsRegisterEndpoint)
if err != nil {
return nil, model.NewAppError("GetAuthorizationServerMetadata", "api.oauth.authorization_server_metadata.invalid_url.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return metadata, nil
}
func (a *App) RegisterOAuthClient(rctx request.CTX, req *model.ClientRegistrationRequest, userID string) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("RegisterOAuthClient", "api.oauth.register_oauth_app.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
app := model.NewOAuthAppFromClientRegistration(req, userID)
oauthApp, err := a.Srv().Store().OAuth().SaveApp(app)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
a.Log().Error("Error saving OAuth app via DCR", mlog.Err(err), mlog.String("name", app.Name))
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("RegisterOAuthClient", "app.oauth.save_app.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("RegisterOAuthClient", "app.oauth.save_app.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return oauthApp, nil
}
// parseOAuthStateTokenExtra parses a token extra string in the format "email:action:cookie".
// Returns an error if the token does not contain exactly 3 colon-separated parts.
func parseOAuthStateTokenExtra(tokenExtra string) (email, action, cookie string, err error) {
parts := strings.Split(tokenExtra, ":")
if len(parts) != 3 {
return "", "", "", fmt.Errorf("invalid token format: expected exactly 3 parts separated by ':', got %d", len(parts))
}
email = parts[0]
action = parts[1]
cookie = parts[2]
return email, action, cookie, nil
}