Skip to content

Commit 5bb8d19

Browse files
techknowlogickjackHay22lunnyKN4CK3Rwxiaoguang
authored
Support SAML authentication (#25165)
Closes #5512 This PR adds basic SAML support - Adds SAML 2.0 as an auth source - Adds SAML configuration documentation - Adds integration test: - Use bare-bones SAML IdP to test protocol flow and test account is linked successfully (only runs on Postgres by default) - Adds documentation for configuring and running SAML integration test locally Future PRs: - Support group mapping - Support auto-registration (account linking) Co-Authored-By: @jackHay22 --------- Co-authored-by: jackHay22 <jack@allspice.io> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: KN4CK3R <admin@oldschoolhack.me> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Jason Song <i@wolfogre.com> Co-authored-by: morphelinho <morphelinho@users.noreply.github.com> Co-authored-by: Zettat123 <zettat123@gmail.com> Co-authored-by: Yarden Shoham <git@yardenshoham.com> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: silverwind <me@silverwind.io>
1 parent c4b0cb4 commit 5bb8d19

37 files changed

+1440
-69
lines changed

.github/workflows/pull-db-tests.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ jobs:
3737
MINIO_ROOT_PASSWORD: 12345678
3838
ports:
3939
- "9000:9000"
40+
simplesaml:
41+
image: allspice/simple-saml
42+
ports:
43+
- "8080:8080"
44+
env:
45+
SIMPLESAMLPHP_SP_ENTITY_ID: http://localhost:3002/user/saml/test-sp/metadata
46+
SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: http://localhost:3002/user/saml/test-sp/acs
47+
SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: http://localhost:3002/user/saml/test-sp/acs
4048
steps:
4149
- uses: actions/checkout@v4
4250
- uses: actions/setup-go@v5

assets/go-licenses.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/content/usage/authentication.en-us.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,72 @@ If set `ENABLE_REVERSE_PROXY_FULL_NAME=true`, a user full name expected in `X-WE
349349
You can also limit the reverse proxy's IP address range with `REVERSE_PROXY_TRUSTED_PROXIES` which default value is `127.0.0.0/8,::1/128`. By `REVERSE_PROXY_LIMIT`, you can limit trusted proxies level.
350350

351351
Notice: Reverse Proxy Auth doesn't support the API. You still need an access token or basic auth to make API requests.
352+
353+
## SAML
354+
355+
### Configuring Gitea as a SAML 2.0 Service Provider
356+
357+
- Navigate to `Site Administration > Identity & Access > Authentication Sources`.
358+
- Click the `Add Authentication Source` button.
359+
- Select `SAML` as the authentication type.
360+
361+
#### Features Not Yet Supported
362+
363+
Currently, auto-registration is not supported for SAML. During the external account linking process the user will be prompted to set a username and email address or link to an existing account.
364+
365+
SAML group mapping is not supported.
366+
367+
#### Settings
368+
369+
- `Authentication Name` **(required)**
370+
371+
- The name of this authentication source (appears in the Gitea ACS and metadata URLs)
372+
373+
- `SAML NameID Format` **(required)**
374+
375+
- This specifies how Identity Provider (IdP) users are mapped to Gitea users. This option will be provider specific.
376+
377+
- `Icon URL` (optional)
378+
379+
- URL of an icon to display on the Sign-In page for this authentication source.
380+
381+
- `[Insecure] Skip Assertion Signature Validation` (optional)
382+
383+
- This option is not recommended and disables integrity verification of IdP SAML assertions.
384+
385+
- `Identity Provider Metadata URL` (optional if XML set)
386+
387+
- The URL of the IdP metadata endpoint.
388+
- This field must be set if `Identity Provider Metadata XML` is left blank.
389+
390+
- `Identity Provider Metadata XML` (optional if URL set)
391+
392+
- The XML returned by the IdP metadata endpoint.
393+
- This field must be set if `Identity Provider Metadata URL` is left blank.
394+
395+
- `Service Provider Certificate` (optional)
396+
397+
- X.509-formatted certificate (with `Service Provider Private Key`) used for signing SAML requests.
398+
- A certificate will be generated if this field is left blank.
399+
400+
- `Service Provider Private Key` (optional)
401+
402+
- DSA/RSA private key (with `Service Provider Certificate`) used for signing SAML requests.
403+
- A private key will be generated if this field is left blank.
404+
405+
- `Email Assertion Key` (optional)
406+
407+
- The SAML assertion key used for the IdP user's email (depends on provider configuration).
408+
409+
- `Name Assertion Key` (optional)
410+
411+
- The SAML assertion key used for the IdP user's nickname (depends on provider configuration).
412+
413+
- `Username Assertion Key` (optional)
414+
415+
- The SAML assertion key used for the IdP user's username (depends on provider configuration).
416+
417+
### Configuring a SAML 2.0 Identity Provider to use Gitea
418+
419+
- The service provider assertion consumer service url will look like: `http(s)://[mydomain]/user/saml/[Authentication Name]/acs`.
420+
- The service provider metadata url will look like: `http(s)://[mydomain]/user/saml/[Authentication Name]/metadata`.

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ require (
9191
github.com/quasoft/websspi v1.1.2
9292
github.com/redis/go-redis/v9 v9.4.0
9393
github.com/robfig/cron/v3 v3.0.1
94+
github.com/russellhaering/gosaml2 v0.9.1
95+
github.com/russellhaering/goxmldsig v1.3.0
9496
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
9597
github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd
9698
github.com/sergi/go-diff v1.3.1
@@ -143,6 +145,7 @@ require (
143145
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
144146
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
145147
github.com/aymerick/douceur v0.2.0 // indirect
148+
github.com/beevik/etree v1.1.0 // indirect
146149
github.com/beorn7/perks v1.0.1 // indirect
147150
github.com/bits-and-blooms/bitset v1.13.0 // indirect
148151
github.com/blevesearch/bleve_index_api v1.1.5 // indirect
@@ -216,6 +219,7 @@ require (
216219
github.com/imdario/mergo v0.3.16 // indirect
217220
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
218221
github.com/jessevdk/go-flags v1.5.0 // indirect
222+
github.com/jonboulle/clockwork v0.3.0 // indirect
219223
github.com/josharian/intern v1.0.0 // indirect
220224
github.com/kevinburke/ssh_config v1.2.0 // indirect
221225
github.com/klauspost/pgzip v1.2.6 // indirect
@@ -225,6 +229,7 @@ require (
225229
github.com/magiconair/properties v1.8.7 // indirect
226230
github.com/mailru/easyjson v0.7.7 // indirect
227231
github.com/markbates/going v1.0.3 // indirect
232+
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
228233
github.com/mattn/go-colorable v0.1.13 // indirect
229234
github.com/mattn/go-runewidth v0.0.15 // indirect
230235
github.com/mholt/acmez v1.2.0 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
130130
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
131131
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
132132
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
133+
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
134+
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
133135
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
134136
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
135137
github.com/bits-and-blooms/bitset v1.1.10/go.mod h1:w0XsmFg8qg6cmpTtJ0z3pKgjTDBMMnI/+I2syrE6XBE=
@@ -566,6 +568,9 @@ github.com/jhillyerd/enmime v1.1.0 h1:ubaIzg68VY7CMCe2YbHe6nkRvU9vujixTkNz3EBvZO
566568
github.com/jhillyerd/enmime v1.1.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
567569
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
568570
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
571+
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
572+
github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg=
573+
github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
569574
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
570575
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
571576
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -634,6 +639,8 @@ github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE
634639
github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o=
635640
github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
636641
github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
642+
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
643+
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
637644
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
638645
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
639646
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -766,12 +773,17 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
766773
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
767774
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
768775
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
776+
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
769777
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
770778
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
771779
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
772780
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
773781
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
774782
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
783+
github.com/russellhaering/gosaml2 v0.9.1 h1:H/whrl8NuSoxyW46Ww5lKPskm+5K+qYLw9afqJ/Zef0=
784+
github.com/russellhaering/gosaml2 v0.9.1/go.mod h1:ja+qgbayxm+0mxBRLMSUuX3COqy+sb0RRhIGun/W2kc=
785+
github.com/russellhaering/goxmldsig v1.3.0 h1:DllIWUgMy0cRUMfGiASiYEa35nsieyD3cigIwLonTPM=
786+
github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
775787
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
776788
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
777789
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

models/auth/oauth2.go

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"crypto/sha256"
99
"encoding/base32"
1010
"encoding/base64"
11+
"encoding/gob"
1112
"fmt"
1213
"net"
1314
"net/url"
@@ -81,6 +82,10 @@ func Init(ctx context.Context) error {
8182
builtinAllClientIDs = append(builtinAllClientIDs, clientID)
8283
}
8384

85+
// This is needed in order to encode and store the struct in the goth/gothic session
86+
// during the process of linking the external user.
87+
gob.Register(LinkAccountUser{})
88+
8489
var registeredApps []*OAuth2Application
8590
if err := db.GetEngine(ctx).In("client_id", builtinAllClientIDs).Find(&registeredApps); err != nil {
8691
return err
@@ -605,21 +610,6 @@ func (err ErrOAuthApplicationNotFound) Unwrap() error {
605610
return util.ErrNotExist
606611
}
607612

608-
// GetActiveOAuth2SourceByName returns a OAuth2 AuthSource based on the given name
609-
func GetActiveOAuth2SourceByName(ctx context.Context, name string) (*Source, error) {
610-
authSource := new(Source)
611-
has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource)
612-
if err != nil {
613-
return nil, err
614-
}
615-
616-
if !has {
617-
return nil, fmt.Errorf("oauth2 source not found, name: %q", name)
618-
}
619-
620-
return authSource, nil
621-
}
622-
623613
func DeleteOAuth2RelictsByUserID(ctx context.Context, userID int64) error {
624614
deleteCond := builder.Select("id").From("oauth2_grant").Where(builder.Eq{"oauth2_grant.user_id": userID})
625615

models/auth/source.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"code.gitea.io/gitea/modules/timeutil"
1515
"code.gitea.io/gitea/modules/util"
1616

17+
"github.com/markbates/goth"
1718
"xorm.io/builder"
1819
"xorm.io/xorm"
1920
"xorm.io/xorm/convert"
@@ -32,6 +33,7 @@ const (
3233
DLDAP // 5
3334
OAuth2 // 6
3435
SSPI // 7
36+
SAML // 8
3537
)
3638

3739
// String returns the string name of the LoginType
@@ -52,6 +54,7 @@ var Names = map[Type]string{
5254
PAM: "PAM",
5355
OAuth2: "OAuth2",
5456
SSPI: "SPNEGO with SSPI",
57+
SAML: "SAML",
5558
}
5659

5760
// Config represents login config as far as the db is concerned
@@ -121,6 +124,12 @@ type Source struct {
121124
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
122125
}
123126

127+
// LinkAccountUser is used to link an external user with a local user
128+
type LinkAccountUser struct {
129+
Type Type
130+
GothUser goth.User
131+
}
132+
124133
// TableName xorm will read the table name from this method
125134
func (Source) TableName() string {
126135
return "login_source"
@@ -180,6 +189,11 @@ func (source *Source) IsSSPI() bool {
180189
return source.Type == SSPI
181190
}
182191

192+
// IsSAML returns true of this source is of the SAML type.
193+
func (source *Source) IsSAML() bool {
194+
return source.Type == SAML
195+
}
196+
183197
// HasTLS returns true of this source supports TLS.
184198
func (source *Source) HasTLS() bool {
185199
hasTLSer, ok := source.Cfg.(HasTLSer)
@@ -392,3 +406,27 @@ func IsErrSourceInUse(err error) bool {
392406
func (err ErrSourceInUse) Error() string {
393407
return fmt.Sprintf("login source is still used by some users [id: %d]", err.ID)
394408
}
409+
410+
// GetActiveAuthProviderSources returns all activated sources
411+
func GetActiveAuthProviderSources(ctx context.Context, authType Type) ([]*Source, error) {
412+
sources := make([]*Source, 0, 1)
413+
if err := db.GetEngine(ctx).Where("is_active = ? and type = ?", true, authType).Find(&sources); err != nil {
414+
return nil, err
415+
}
416+
return sources, nil
417+
}
418+
419+
// GetActiveAuthSourceByName returns an AuthSource based on the given name and type
420+
func GetActiveAuthSourceByName(ctx context.Context, name string, authType Type) (*Source, error) {
421+
authSource := new(Source)
422+
has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, authType, true).Get(authSource)
423+
if err != nil {
424+
return nil, err
425+
}
426+
427+
if !has {
428+
return nil, fmt.Errorf("auth source not found, name: %q", name)
429+
}
430+
431+
return authSource, nil
432+
}

options/locale/locale_en-US.ini

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,9 @@ Content = Content
522522
SSPISeparatorReplacement = Separator
523523
SSPIDefaultLanguage = Default Language
524524
525+
SAMLMetadata = Either SAML Identity Provider metadata URL or XML
526+
SAMLMetadataURL = SAML Identity Provider metadata URL is invalid
527+
525528
require_error = ` cannot be empty.`
526529
alpha_dash_error = ` should contain only alphanumeric, dash ('-') and underscore ('_') characters.`
527530
alpha_dash_dot_error = ` should contain only alphanumeric, dash ('-'), underscore ('_') and dot ('.') characters.`
@@ -3026,7 +3029,18 @@ auths.sspi_separator_replacement = Separator to use instead of \, / and @
30263029
auths.sspi_separator_replacement_helper = The character to use to replace the separators of down-level logon names (eg. the \ in "DOMAIN\user") and user principal names (eg. the @ in "user@example.org").
30273030
auths.sspi_default_language = Default user language
30283031
auths.sspi_default_language_helper = Default language for users automatically created by SSPI auth method. Leave empty if you prefer language to be automatically detected.
3032+
auths.saml_nameidformat = SAML NameID Format
3033+
auths.saml_identity_provider_metadata_url = Identity Provider Metadata URL
3034+
auths.saml_identity_provider_metadata = Identity Provider Metadata XML
3035+
auths.saml_insecure_skip_assertion_signature_validation = [Insecure] Skip Assertion Signature Validation
3036+
auths.saml_service_provider_certificate = Service Provider Certificate
3037+
auths.saml_service_provider_private_key = Service Provider Private Key
3038+
auths.saml_identity_provider_email_assertion_key = Email Assertion Key
3039+
auths.saml_identity_provider_name_assertion_key = Name Assertion Key
3040+
auths.saml_identity_provider_username_assertion_key = Username Assertion Key
3041+
auths.saml_icon_url = Icon URL
30293042
auths.tips = Tips
3043+
auths.tips.saml = Documentation can be found at https://docs.gitea.com/usage/authentication#saml
30303044
auths.tips.oauth2.general = OAuth2 Authentication
30313045
auths.tips.oauth2.general.tip = When registering a new OAuth2 authentication, the callback/redirect URL should be:
30323046
auths.tip.oauth2_provider = OAuth2 Provider

routers/init.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
actions_service "code.gitea.io/gitea/services/actions"
3636
"code.gitea.io/gitea/services/auth"
3737
"code.gitea.io/gitea/services/auth/source/oauth2"
38+
"code.gitea.io/gitea/services/auth/source/saml"
3839
"code.gitea.io/gitea/services/automerge"
3940
"code.gitea.io/gitea/services/cron"
4041
feed_service "code.gitea.io/gitea/services/feed"
@@ -138,6 +139,7 @@ func InitWebInstalled(ctx context.Context) {
138139
log.Info("ORM engine initialization successful!")
139140
mustInit(system.Init)
140141
mustInitCtx(ctx, oauth2.Init)
142+
mustInitCtx(ctx, saml.Init)
141143

142144
mustInit(release_service.Init)
143145

0 commit comments

Comments
 (0)