mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-16 14:03:00 -06:00
further oidc
This commit is contained in:
parent
8e0d32d3e1
commit
81206d93f3
14 changed files with 227 additions and 70 deletions
|
|
@ -31,12 +31,6 @@ func oidcFlags(flagNames, envNames config.Flags, defaults config.Defaults) []cli
|
|||
Value: defaults.OIDCEnabled,
|
||||
EnvVars: []string{envNames.OIDCEnabled},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagNames.OIDCIdpID,
|
||||
Usage: "ID of the OIDC identity provider.",
|
||||
Value: defaults.OIDCIdpID,
|
||||
EnvVars: []string{envNames.OIDCIdpID},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagNames.OIDCIdpName,
|
||||
Usage: "Name of the OIDC identity provider. Will be shown to the user when logging in.",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
###########################
|
||||
##### GENERAL CONFIG ######
|
||||
###########################
|
||||
|
||||
# String. Log level to use throughout the application. Must be lower-case.
|
||||
# Options: ["trace","debug","info","warn","error","fatal"]
|
||||
# Default: "info"
|
||||
|
|
@ -54,8 +55,10 @@ protocol: "https"
|
|||
############################
|
||||
##### DATABASE CONFIG ######
|
||||
############################
|
||||
|
||||
# Config pertaining to the Gotosocial database connection
|
||||
db:
|
||||
|
||||
# String. Database type.
|
||||
# Options: ["postgres"]
|
||||
# Default: "postgres"
|
||||
|
|
@ -105,8 +108,10 @@ db:
|
|||
###############################
|
||||
##### WEB TEMPLATE CONFIG #####
|
||||
###############################
|
||||
|
||||
# Config pertaining to templating of web pages/email notifications and the like
|
||||
template:
|
||||
|
||||
# String. Directory from which gotosocial will attempt to load html templates (.tmpl files).
|
||||
# Examples: ["/some/absolute/path/", "./relative/path/", "../../some/weird/path/"]
|
||||
# Default: "./web/template/"
|
||||
|
|
@ -120,8 +125,10 @@ template:
|
|||
###########################
|
||||
##### ACCOUNTS CONFIG #####
|
||||
###########################
|
||||
|
||||
# Config pertaining to creation and maintenance of accounts on the server, as well as defaults for new accounts.
|
||||
accounts:
|
||||
|
||||
# Bool. Do we want people to be able to just submit sign up requests, or do we want invite only?
|
||||
# Options: [true, false]
|
||||
# Default: true
|
||||
|
|
@ -140,8 +147,10 @@ accounts:
|
|||
########################
|
||||
##### MEDIA CONFIG #####
|
||||
########################
|
||||
|
||||
# Config pertaining to user media uploads (videos, image, image descriptions).
|
||||
media:
|
||||
|
||||
# Int. Maximum allowed image upload size in bytes.
|
||||
# Examples: [2097152, 10485760]
|
||||
# Default: 2097152 -- aka 2MB
|
||||
|
|
@ -165,8 +174,10 @@ media:
|
|||
##########################
|
||||
##### STORAGE CONFIG #####
|
||||
##########################
|
||||
|
||||
# Config pertaining to storage of user-created uploads (videos, images, etc).
|
||||
storage:
|
||||
|
||||
# String. Type of storage backend to use.
|
||||
# Examples: ["local", "s3"]
|
||||
# Default: "local" (storage on local disk)
|
||||
|
|
@ -203,8 +214,10 @@ storage:
|
|||
###########################
|
||||
##### STATUSES CONFIG #####
|
||||
###########################
|
||||
|
||||
# Config pertaining to the creation of statuses/posts, and permitted limits.
|
||||
statuses:
|
||||
|
||||
# Int. Maximum amount of characters permitted for a new status.
|
||||
# Note that going way higher than the default might break federation.
|
||||
# Examples: [140, 500, 5000]
|
||||
|
|
@ -238,8 +251,10 @@ statuses:
|
|||
##############################
|
||||
##### LETSENCRYPT CONFIG #####
|
||||
##############################
|
||||
|
||||
# Config pertaining to the automatic acquisition and use of LetsEncrypt HTTPS certificates.
|
||||
letsEncrypt:
|
||||
|
||||
# Bool. Whether or not letsencrypt should be enabled for the server.
|
||||
# If true, the server will serve on port 443 (https) and obtain letsencrypt
|
||||
# certificates automatically.
|
||||
|
|
@ -248,7 +263,7 @@ letsEncrypt:
|
|||
# You should only change this if you want to serve GoToSocial behind a reverse proxy
|
||||
# like Traefik, HAProxy, or Nginx.
|
||||
# Options: [true, false]
|
||||
# Default: true
|
||||
# Default: true
|
||||
enabled: true
|
||||
|
||||
# String. Directory in which to store LetsEncrypt certificates.
|
||||
|
|
@ -265,3 +280,58 @@ letsEncrypt:
|
|||
# Examples: ["admin@example.org"]
|
||||
# Default: ""
|
||||
emailAddress: ""
|
||||
|
||||
#######################
|
||||
##### OIDC CONFIG #####
|
||||
#######################
|
||||
|
||||
# Config for authentication with an external OIDC provider (Dex, Google, Auth0, etc).
|
||||
oidc:
|
||||
|
||||
# Bool. Enable authentication with external OIDC provider. If set to true, then
|
||||
# the other OIDC options must be set as well. If this is set to false, then the standard
|
||||
# internal oauth flow will be used, where users sign in to GtS with username/password.
|
||||
# Options: [true, false]
|
||||
# Default: false
|
||||
enabled: false
|
||||
|
||||
# String. Name of the oidc idp (identity provider). This will be shown to users when
|
||||
# they log in.
|
||||
# Examples: ["Google", "Dex", "Auth0"]
|
||||
# Default: ""
|
||||
idpName: ""
|
||||
|
||||
# Bool. Skip the normal verification flow of tokens returned from the OIDC provider, ie.,
|
||||
# don't check the expiry or signature. This should only be used in debugging or testing,
|
||||
# never ever in a production environment as it's extremely unsafe!
|
||||
# Options: [true, false]
|
||||
# Default: false
|
||||
skipVerification: false
|
||||
|
||||
# String. The OIDC issuer URI. This is where GtS will redirect users to for login.
|
||||
# Typically this will look like a standard web URL.
|
||||
# Examples: ["https://auth.example.org", "https://example.org/auth"]
|
||||
# Default: ""
|
||||
issuer: ""
|
||||
|
||||
# String. The ID for this client as registered with the OIDC provider.
|
||||
# Examples: ["some-client-id", "fda3772a-ad35-41c9-9a59-f1943ad18f54"]
|
||||
# Default: ""
|
||||
clientID: ""
|
||||
|
||||
# String. The secret for this client as registered with the OIDC provider.
|
||||
# Examples: ["super-secret-business", "79379cf5-8057-426d-bb83-af504d98a7b0"]
|
||||
# Default: ""
|
||||
clientSecret: ""
|
||||
|
||||
# Array of string. Scopes to request from the OIDC provider. The returned values will be used to
|
||||
# populate users created in GtS as a result of the authentication flow. 'openid' and 'email' are required.
|
||||
# 'profile' is used to extract a username for the newly created user.
|
||||
# 'groups' is optional and can be used to determine if a user is an admin (if they're in the group 'admin' or 'admins').
|
||||
# Examples: See eg., https://auth0.com/docs/scopes/openid-connect-scopes
|
||||
# Default: ["openid", "email", "profile", "groups"]
|
||||
scopes:
|
||||
- "openid"
|
||||
- "email"
|
||||
- "profile"
|
||||
- "groups"
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ const (
|
|||
sessionResponseType = "response_type"
|
||||
sessionCode = "code"
|
||||
sessionScope = "scope"
|
||||
sessionState = "state"
|
||||
)
|
||||
|
||||
var sessionKeys []string = []string{
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -46,6 +47,8 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
|||
form := &model.OAuthAuthorize{}
|
||||
if err := c.Bind(form); err != nil {
|
||||
l.Debugf("invalid auth form: %s", err)
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
l.Tracef("parsed auth form: %+v", form)
|
||||
|
|
@ -69,6 +72,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
|||
ClientID: clientID,
|
||||
}
|
||||
if err := m.db.GetWhere([]db.Where{{Key: sessionClientID, Value: app.ClientID}}, app); err != nil {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)})
|
||||
return
|
||||
}
|
||||
|
|
@ -78,6 +82,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
|||
ID: userID,
|
||||
}
|
||||
if err := m.db.GetByID(user.ID, user); err != nil {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
|
@ -87,6 +92,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
|||
}
|
||||
|
||||
if err := m.db.GetByID(acct.ID, acct); err != nil {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
|
@ -94,11 +100,13 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
|||
// Finally we should also get the redirect and scope of this particular request, as stored in the session.
|
||||
redirect, ok := s.Get(sessionRedirectURI).(string)
|
||||
if !ok || redirect == "" {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "no redirect_uri found in session"})
|
||||
return
|
||||
}
|
||||
scope, ok := s.Get(sessionScope).(string)
|
||||
if !ok || scope == "" {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "no scope found in session"})
|
||||
return
|
||||
}
|
||||
|
|
@ -128,48 +136,42 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
|
|||
// recreate it on the request so that it can be used further by the oauth2 library.
|
||||
// So first fetch all the values from the session.
|
||||
|
||||
errs := []string{}
|
||||
|
||||
forceLogin, ok := s.Get(sessionForceLogin).(string)
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "session missing force_login"})
|
||||
return
|
||||
errs = append(errs, "session missing force_login")
|
||||
}
|
||||
|
||||
responseType, ok := s.Get(sessionResponseType).(string)
|
||||
if !ok || responseType == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "session missing response_type"})
|
||||
return
|
||||
errs = append(errs, "session missing response_type")
|
||||
}
|
||||
|
||||
clientID, ok := s.Get(sessionClientID).(string)
|
||||
if !ok || clientID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "session missing client_id"})
|
||||
return
|
||||
errs = append(errs, "session missing client_id")
|
||||
}
|
||||
|
||||
redirectURI, ok := s.Get(sessionRedirectURI).(string)
|
||||
if !ok || redirectURI == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "session missing redirect_uri"})
|
||||
return
|
||||
errs = append(errs, "session missing redirect_uri")
|
||||
}
|
||||
|
||||
scope, ok := s.Get(sessionScope).(string)
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "session missing scope"})
|
||||
return
|
||||
errs = append(errs, "session missing scope")
|
||||
}
|
||||
|
||||
userID, ok := s.Get(sessionUserID).(string)
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "session missing userid"})
|
||||
return
|
||||
errs = append(errs, "session missing userid")
|
||||
}
|
||||
|
||||
// we're done with the session so we can clear it now
|
||||
for _, key := range sessionKeys {
|
||||
s.Delete(key)
|
||||
}
|
||||
if err := s.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
m.clearSession(s)
|
||||
|
||||
if len(errs) != 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": strings.Join(errs, ": ")})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,21 +19,81 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oidc"
|
||||
)
|
||||
|
||||
// CallbackGETHandler parses a token from an external auth provider.
|
||||
func (m *Module) CallbackGETHandler(c *gin.Context) {
|
||||
state := c.Query(callbackStateParam)
|
||||
code := c.Query(callbackCodeParam)
|
||||
s := sessions.Default(c)
|
||||
|
||||
claims, err := m.idp.HandleCallback(c.Request.Context(), state, code)
|
||||
if err != nil {
|
||||
c.String(http.StatusForbidden, err.Error())
|
||||
// first make sure the state set in the cookie is the same as the state returned from the external provider
|
||||
state := c.Query(callbackStateParam)
|
||||
if state == "" {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "state query not found on callback"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, claims)
|
||||
savedStateI := s.Get(sessionState)
|
||||
savedState, ok := savedStateI.(string)
|
||||
if !ok {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"})
|
||||
return
|
||||
}
|
||||
|
||||
if state != savedState {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "state mismatch"})
|
||||
return
|
||||
}
|
||||
|
||||
code := c.Query(callbackCodeParam)
|
||||
|
||||
claims, err := m.idp.HandleCallback(c.Request.Context(), code)
|
||||
if err != nil {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.parseUserFromClaims(claims)
|
||||
if err != nil {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
s.Set(sessionUserID, user.ID)
|
||||
if err := s.Save(); err != nil {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, OauthAuthorizePath)
|
||||
}
|
||||
|
||||
func (m *Module) parseUserFromClaims(claims *oidc.Claims) (*gtsmodel.User, error) {
|
||||
if claims.Email == "" {
|
||||
return nil, errors.New("no email returned in claims")
|
||||
}
|
||||
// see if we already have a user for this email address
|
||||
|
||||
|
||||
if claims.Name == "" {
|
||||
return nil, errors.New("no name returned in claims")
|
||||
}
|
||||
username := ""
|
||||
nameParts := strings.Split(claims.Name, " ")
|
||||
for i, n := range nameParts {
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
|
@ -41,9 +42,18 @@ type login struct {
|
|||
func (m *Module) SignInGETHandler(c *gin.Context) {
|
||||
l := m.log.WithField("func", "SignInGETHandler")
|
||||
l.Trace("entering sign in handler")
|
||||
if m.idp != nil && m.config.OIDCConfig.Issuer != "" {
|
||||
l.Debug("redirecting to external idp at %s", m.config.OIDCConfig.Issuer)
|
||||
c.Redirect(http.StatusFound, m.config.OIDCConfig.Issuer)
|
||||
if m.idp != nil {
|
||||
s := sessions.Default(c)
|
||||
state := uuid.NewString()
|
||||
s.Set(sessionState, state)
|
||||
if err := s.Save(); err != nil {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
redirect := m.idp.AuthCodeURL(state)
|
||||
l.Debugf("redirecting to external idp at %s", redirect)
|
||||
c.Redirect(http.StatusSeeOther, redirect)
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
|
||||
|
|
@ -58,6 +68,7 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) {
|
|||
form := &login{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
m.clearSession(s)
|
||||
return
|
||||
}
|
||||
l.Tracef("parsed form: %+v", form)
|
||||
|
|
@ -65,12 +76,14 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) {
|
|||
userid, err := m.ValidatePassword(form.Email, form.Password)
|
||||
if err != nil {
|
||||
c.String(http.StatusForbidden, err.Error())
|
||||
m.clearSession(s)
|
||||
return
|
||||
}
|
||||
|
||||
s.Set(sessionUserID, userid)
|
||||
if err := s.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
m.clearSession(s)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
20
internal/api/client/auth/util.go
Normal file
20
internal/api/client/auth/util.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||
)
|
||||
|
||||
func (m *Module) clearSession(s sessions.Session) {
|
||||
for _, key := range sessionKeys {
|
||||
s.Delete(key)
|
||||
}
|
||||
|
||||
newOptions := router.SessionOptions(m.config)
|
||||
newOptions.MaxAge = -1 // instruct browser to delete cookie immediately
|
||||
s.Options(newOptions)
|
||||
|
||||
if err := s.Save(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -275,10 +275,6 @@ func (c *Config) ParseCLIFlags(f KeyedFlags, version string) error {
|
|||
c.OIDCConfig.Enabled = f.Bool(fn.OIDCEnabled)
|
||||
}
|
||||
|
||||
if c.OIDCConfig.IDPID == "" || f.IsSet(fn.OIDCIdpID) {
|
||||
c.OIDCConfig.IDPID = f.String(fn.OIDCIdpID)
|
||||
}
|
||||
|
||||
if c.OIDCConfig.IDPName == "" || f.IsSet(fn.OIDCIdpName) {
|
||||
c.OIDCConfig.IDPName = f.String(fn.OIDCIdpName)
|
||||
}
|
||||
|
|
@ -372,7 +368,6 @@ type Flags struct {
|
|||
LetsEncryptEmailAddress string
|
||||
|
||||
OIDCEnabled string
|
||||
OIDCIdpID string
|
||||
OIDCIdpName string
|
||||
OIDCSkipVerification string
|
||||
OIDCIssuer string
|
||||
|
|
@ -429,7 +424,6 @@ type Defaults struct {
|
|||
LetsEncryptEmailAddress string
|
||||
|
||||
OIDCEnabled bool
|
||||
OIDCIdpID string
|
||||
OIDCIdpName string
|
||||
OIDCSkipVerification bool
|
||||
OIDCIssuer string
|
||||
|
|
@ -487,7 +481,6 @@ func GetFlagNames() Flags {
|
|||
LetsEncryptEmailAddress: "letsencrypt-email",
|
||||
|
||||
OIDCEnabled: "oidc-enabled",
|
||||
OIDCIdpID: "oidc-idp-id",
|
||||
OIDCIdpName: "oidc-idp-name",
|
||||
OIDCSkipVerification: "oidc-skip-verification",
|
||||
OIDCIssuer: "oidc-issuer",
|
||||
|
|
@ -546,7 +539,6 @@ func GetEnvNames() Flags {
|
|||
LetsEncryptEmailAddress: "GTS_LETSENCRYPT_EMAIL",
|
||||
|
||||
OIDCEnabled: "GTS_OIDC_ENABLED",
|
||||
OIDCIdpID: "GTS_OIDC_IDP_ID",
|
||||
OIDCIdpName: "GTS_OIDC_IDP_NAME",
|
||||
OIDCSkipVerification: "GTS_OIDC_SKIP_VERIFICATION",
|
||||
OIDCIssuer: "GTS_OIDC_ISSUER",
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ func TestDefault() *Config {
|
|||
},
|
||||
OIDCConfig: &OIDCConfig{
|
||||
Enabled: defaults.OIDCEnabled,
|
||||
IDPID: defaults.OIDCIdpID,
|
||||
IDPName: defaults.OIDCIdpName,
|
||||
SkipVerification: defaults.OIDCSkipVerification,
|
||||
Issuer: defaults.OIDCIssuer,
|
||||
|
|
@ -121,7 +120,6 @@ func Default() *Config {
|
|||
},
|
||||
OIDCConfig: &OIDCConfig{
|
||||
Enabled: defaults.OIDCEnabled,
|
||||
IDPID: defaults.OIDCIdpID,
|
||||
IDPName: defaults.OIDCIdpName,
|
||||
SkipVerification: defaults.OIDCSkipVerification,
|
||||
Issuer: defaults.OIDCIssuer,
|
||||
|
|
@ -181,7 +179,6 @@ func GetDefaults() Defaults {
|
|||
LetsEncryptEmailAddress: "",
|
||||
|
||||
OIDCEnabled: false,
|
||||
OIDCIdpID: "",
|
||||
OIDCIdpName: "",
|
||||
OIDCSkipVerification: false,
|
||||
OIDCIssuer: "",
|
||||
|
|
@ -235,5 +232,13 @@ func GetTestDefaults() Defaults {
|
|||
LetsEncryptEnabled: false,
|
||||
LetsEncryptCertDir: "",
|
||||
LetsEncryptEmailAddress: "",
|
||||
|
||||
OIDCEnabled: false,
|
||||
OIDCIdpName: "",
|
||||
OIDCSkipVerification: false,
|
||||
OIDCIssuer: "",
|
||||
OIDCClientID: "",
|
||||
OIDCClientSecret: "",
|
||||
OIDCScopes: []string{oidc.ScopeOpenID, "profile", "email", "groups"},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ package config
|
|||
// OIDCConfig contains configuration values for openID connect (oauth) authorization by an external service such as Dex.
|
||||
type OIDCConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
IDPID string `yaml:"idpId"`
|
||||
IDPName string `yaml:"idpName"`
|
||||
SkipVerification bool `yaml:"skipVerification"`
|
||||
Issuer string `yaml:"issuer"`
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ package oidc
|
|||
|
||||
// Claims represents claims as found in an id_token returned from an OIDC flow.
|
||||
type Claims struct {
|
||||
Email string `json:"email"`
|
||||
Groups []string `json:"groups"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Groups []string `json:"groups"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,8 @@ import (
|
|||
"fmt"
|
||||
)
|
||||
|
||||
func (i *idp) HandleCallback(ctx context.Context, state string, code string) (*Claims, error) {
|
||||
func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, error) {
|
||||
l := i.log.WithField("func", "HandleCallback")
|
||||
|
||||
if state == "" {
|
||||
return nil, errors.New("state was empty string")
|
||||
}
|
||||
|
||||
if code == "" {
|
||||
return nil, errors.New("code was empty string")
|
||||
}
|
||||
|
|
@ -48,7 +43,7 @@ func (i *idp) HandleCallback(ctx context.Context, state string, code string) (*C
|
|||
if !ok {
|
||||
return nil, errors.New("no id_token in oauth2token")
|
||||
}
|
||||
l.Debug("raw id token: %s", rawIDToken)
|
||||
l.Debugf("raw id token: %s", rawIDToken)
|
||||
|
||||
// Parse and verify ID Token payload.
|
||||
l.Debug("verifying id_token")
|
||||
|
|
@ -66,3 +61,7 @@ func (i *idp) HandleCallback(ctx context.Context, state string, code string) (*C
|
|||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (i *idp) AuthCodeURL(state string) string {
|
||||
return i.oauth2Config.AuthCodeURL(state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,13 +31,11 @@ import (
|
|||
const (
|
||||
// CallbackPath is the API path for receiving callback tokens from external OIDC providers
|
||||
CallbackPath = "/auth/callback"
|
||||
profileScope = "profile"
|
||||
emailScope = "email"
|
||||
groupsScope = "groups"
|
||||
)
|
||||
|
||||
type IDP interface {
|
||||
HandleCallback(ctx context.Context, state string, code string) (*Claims, error)
|
||||
HandleCallback(ctx context.Context, code string) (*Claims, error)
|
||||
AuthCodeURL(state string) string
|
||||
}
|
||||
|
||||
type idp struct {
|
||||
|
|
@ -55,9 +53,6 @@ func NewIDP(config *config.Config, log *logrus.Logger) (IDP, error) {
|
|||
}
|
||||
|
||||
// validate config fields
|
||||
if config.OIDCConfig.IDPID == "" {
|
||||
return nil, fmt.Errorf("not set: IDPID")
|
||||
}
|
||||
if config.OIDCConfig.IDPName == "" {
|
||||
return nil, fmt.Errorf("not set: IDPName")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,18 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
)
|
||||
|
||||
// SessionOptions returns the standard set of options to use for each session.
|
||||
func SessionOptions(cfg *config.Config) sessions.Options {
|
||||
return sessions.Options{
|
||||
Path: "/",
|
||||
Domain: cfg.Host,
|
||||
MaxAge: 120, // 2 minutes
|
||||
Secure: true, // only use cookie over https
|
||||
HttpOnly: true, // exclude javascript from inspecting cookie
|
||||
SameSite: http.SameSiteStrictMode, // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1
|
||||
}
|
||||
}
|
||||
|
||||
func useSession(cfg *config.Config, dbService db.DB, engine *gin.Engine) error {
|
||||
// check if we have a saved router session already
|
||||
routerSessions := []*gtsmodel.RouterSession{}
|
||||
|
|
@ -64,14 +76,7 @@ func useSession(cfg *config.Config, dbService db.DB, engine *gin.Engine) error {
|
|||
}
|
||||
|
||||
store := memstore.NewStore(rs.Auth, rs.Crypt)
|
||||
store.Options(sessions.Options{
|
||||
Path: "/",
|
||||
Domain: cfg.Host,
|
||||
MaxAge: 120, // 2 minutes
|
||||
Secure: true, // only use cookie over https
|
||||
HttpOnly: true, // exclude javascript from inspecting cookie
|
||||
SameSite: http.SameSiteStrictMode, // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1
|
||||
})
|
||||
store.Options(SessionOptions(cfg))
|
||||
sessionName := fmt.Sprintf("gotosocial-%s", cfg.Host)
|
||||
engine.Use(sessions.Sessions(sessionName, store))
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue