Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable configuration of SAML authentication sources (#5512) #25132

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions models/auth/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
DLDAP // 5
OAuth2 // 6
SSPI // 7
SAML // 8
)

// String returns the string name of the LoginType
Expand All @@ -50,6 +51,7 @@ var Names = map[Type]string{
PAM: "PAM",
OAuth2: "OAuth2",
SSPI: "SPNEGO with SSPI",
SAML: "SAML",
}

// Config represents login config as far as the db is concerned
Expand Down Expand Up @@ -148,43 +150,48 @@ func (source *Source) TypeName() string {
return Names[source.Type]
}

// IsLDAP returns true of this source is of the LDAP type.
// IsLDAP returns true if this source is of the LDAP type.
func (source *Source) IsLDAP() bool {
return source.Type == LDAP
}

// IsDLDAP returns true of this source is of the DLDAP type.
// IsDLDAP returns true if this source is of the DLDAP type.
func (source *Source) IsDLDAP() bool {
return source.Type == DLDAP
}

// IsSMTP returns true of this source is of the SMTP type.
// IsSMTP returns true if this source is of the SMTP type.
func (source *Source) IsSMTP() bool {
return source.Type == SMTP
}

// IsPAM returns true of this source is of the PAM type.
// IsPAM returns true if this source is of the PAM type.
func (source *Source) IsPAM() bool {
return source.Type == PAM
}

// IsOAuth2 returns true of this source is of the OAuth2 type.
// IsOAuth2 returns true if this source is of the OAuth2 type.
func (source *Source) IsOAuth2() bool {
return source.Type == OAuth2
}

// IsSSPI returns true of this source is of the SSPI type.
// IsSSPI returns true if this source is of the SSPI type.
func (source *Source) IsSSPI() bool {
return source.Type == SSPI
}

// HasTLS returns true of this source supports TLS.
// IsSAML returns true if this source is of the SAML type.
func (source *Source) IsSAML() bool {
return source.Type == SAML
}

// HasTLS returns true if this source supports TLS.
func (source *Source) HasTLS() bool {
hasTLSer, ok := source.Cfg.(HasTLSer)
return ok && hasTLSer.HasTLS()
}

// UseTLS returns true of this source is configured to use TLS.
// UseTLS returns true if this source is configured to use TLS.
func (source *Source) UseTLS() bool {
useTLSer, ok := source.Cfg.(UseTLSer)
return ok && useTLSer.UseTLS()
Expand Down
5 changes: 5 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2895,6 +2895,11 @@ auths.sspi_separator_replacement = Separator to use instead of \, / and @
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").
auths.sspi_default_language = Default user language
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.
auths.saml.issuer = IdP Issuer ID
auths.saml.login = IdP Login URL
auths.saml.logout = IdP Logout URL
auths.saml.certificate = IdP Certificate
auths.saml.certificate_error = %s is not a valid Base64-encoded certificate.
auths.tips = Tips
auths.tips.oauth2.general = OAuth2 Authentication
auths.tips.oauth2.general.tip = When registering a new OAuth2 authentication, the callback/redirect URL should be: <host>/user/oauth2/<Authentication Name>/callback
Expand Down
67 changes: 67 additions & 0 deletions routers/web/admin/auths.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package admin

import (
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"net/http"
Expand All @@ -24,6 +26,7 @@ import (
"code.gitea.io/gitea/services/auth/source/ldap"
"code.gitea.io/gitea/services/auth/source/oauth2"
pam_service "code.gitea.io/gitea/services/auth/source/pam"
"code.gitea.io/gitea/services/auth/source/saml"
"code.gitea.io/gitea/services/auth/source/smtp"
"code.gitea.io/gitea/services/auth/source/sspi"
"code.gitea.io/gitea/services/forms"
Expand Down Expand Up @@ -71,6 +74,7 @@ var (
{auth.SMTP.String(), auth.SMTP},
{auth.OAuth2.String(), auth.OAuth2},
{auth.SSPI.String(), auth.SSPI},
{auth.SAML.String(), auth.SAML},
}
if pam.Supported {
items = append(items, dropdownItem{auth.Names[auth.PAM], auth.PAM})
Expand Down Expand Up @@ -231,6 +235,56 @@ func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi
}, nil
}

// parseSAMLConfig verifies that required fields are supplied and that fields are properly typed (URLs are URLs, certificate is certificate)
func parseSAMLConfig(ctx *context.Context, form forms.AuthenticationForm) (*saml.Source, error) {
if util.IsEmptyString(form.SAMLIssuer) {
ctx.Data["Err_SAMLIssuer"] = true
return nil, errors.New(ctx.Tr("admin.auths.saml.issuer") + ctx.Tr("form.require_error"))
}

if util.IsEmptyString(form.SAMLLogin) {
ctx.Data["Err_SAMLLogin"] = true
return nil, errors.New(ctx.Tr("admin.auths.saml.login") + ctx.Tr("form.require_error"))
}
_, err := url.ParseRequestURI(form.SAMLLogin)
if err != nil {
ctx.Data["Err_SAMLLogin"] = true
return nil, errors.New(ctx.Tr("admin.auths.saml.login") + " " + ctx.Tr("form.url_error", form.SAMLLogin))
}

if !util.IsEmptyString(form.SAMLLogout) {
_, err := url.ParseRequestURI(form.SAMLLogout)
if err != nil {
ctx.Data["Err_SAMLLogout"] = true
return nil, errors.New(ctx.Tr("admin.auths.saml.logout") + " " + ctx.Tr("form.url_error", form.SAMLLogout))
}
}

if util.IsEmptyString(form.SAMLCertificate) {
ctx.Data["Err_SAMLCertificate"] = true
return nil, errors.New(ctx.Tr("admin.auths.saml.certificate") + ctx.Tr("form.require_error"))
}
certData, err := base64.StdEncoding.DecodeString(form.SAMLCertificate)
if err != nil {
ctx.Data["Err_SAMLCertificate"] = true
return nil, errors.New(ctx.Tr("admin.auths.saml.certificate_error", ctx.Tr("admin.auths.saml.certificate")))
}
_, err = x509.ParseCertificate(certData)
if err != nil {
ctx.Data["Err_SAMLCertificate"] = true
return nil, errors.New(ctx.Tr("admin.auths.saml.certificate_error", ctx.Tr("admin.auths.saml.certificate")))
}

return &saml.Source{
IdPIssuer: form.SAMLIssuer,
IdPLogin: form.SAMLLogin,
IdPLogout: form.SAMLLogout,
IdPCertificate: form.SAMLCertificate,

SkipLocalTwoFA: form.SkipLocalTwoFA,
}, nil
}

// NewAuthSourcePost response for adding an auth source
func NewAuthSourcePost(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
Expand Down Expand Up @@ -290,6 +344,13 @@ func NewAuthSourcePost(ctx *context.Context) {
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
return
}
case auth.SAML:
var err error
config, err = parseSAMLConfig(ctx, form)
if err != nil {
ctx.RenderWithErr(err.Error(), tplAuthNew, form)
return
}
default:
ctx.Error(http.StatusBadRequest)
return
Expand Down Expand Up @@ -412,6 +473,12 @@ func EditAuthSourcePost(ctx *context.Context) {
ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
return
}
case auth.SAML:
config, err = parseSAMLConfig(ctx, form)
if err != nil {
ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
return
}
default:
ctx.Error(http.StatusBadRequest)
return
Expand Down
36 changes: 36 additions & 0 deletions services/auth/source/saml/source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package saml

import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/json"
)

// Source stores the configuration for a SAML authentication source
type Source struct {
IdPIssuer string
IdPLogin string
IdPLogout string
IdPCertificate string

SkipLocalTwoFA bool `json:",omitempty"`

// reference to the authSource
authSource *auth.Source
}

// FromDB fills up an OAuth2Config from serialized format.
func (source *Source) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &source)
}

// ToDB exports an SMTPConfig to a serialized format.
func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source)
}

func init() {
auth.RegisterTypeConfig(auth.SAML, &Source{})
}
6 changes: 5 additions & 1 deletion services/forms/auth_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
// AuthenticationForm form for authentication
type AuthenticationForm struct {
ID int64
Type int `binding:"Range(2,7)"`
Type int `binding:"Range(2,8)"`
Name string `binding:"Required;MaxSize(30)"`
Host string
Port int
Expand Down Expand Up @@ -80,6 +80,10 @@ type AuthenticationForm struct {
SSPIStripDomainNames bool
SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"`
SSPIDefaultLanguage string
SAMLIssuer string
SAMLLogin string
SAMLLogout string
SAMLCertificate string
GroupTeamMap string `binding:"ValidGroupTeamMap"`
GroupTeamMapRemoval bool
}
Expand Down
29 changes: 29 additions & 0 deletions templates/admin/auth/edit.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,35 @@
<p class="help">{{.locale.Tr "admin.auths.sspi_default_language_helper"}}</p>
</div>
{{end}}

<!-- SAML -->
{{if .Source.IsSAML}}
{{$cfg:=.Source.Cfg}}
<div class="required field">
<label for="saml_issuer">{{.locale.Tr "admin.auths.saml.issuer"}}</label>
<input id="saml_issuer" name="saml_issuer" value="{{$cfg.IdPIssuer}}">
</div>
<div class="required field">
<label for="saml_login">{{.locale.Tr "admin.auths.saml.login"}}</label>
<input id="saml_login" name="saml_login" value="{{$cfg.IdPLogin}}">
</div>
<div class="optional field">
<label for="saml_logout">{{.locale.Tr "admin.auths.saml.logout"}}</label>
<input id="saml_logout" name="saml_logout" value="{{$cfg.IdPLogout}}">
</div>
<div class="required field">
<label>{{.locale.Tr "admin.auths.saml.certificate"}}</label>
<textarea name="saml_certificate" rows="5">{{$cfg.IdPCertificate}}</textarea>
</div>
<div class="optional field">
<div class="ui checkbox">
<label for="skip_local_two_fa"><strong>{{.locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if $cfg.SkipLocalTwoFA}}checked{{end}}>
<p class="help">{{.locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
</div>
</div>
{{end}}

{{if .Source.IsLDAP}}
<div class="inline field">
<div class="ui checkbox">
Expand Down
3 changes: 3 additions & 0 deletions templates/admin/auth/new.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
<!-- SSPI -->
{{template "admin/auth/source/sspi" .}}

<!-- SAML -->
{{template "admin/auth/source/saml" .}}

<div class="ldap field">
<div class="ui checkbox">
<label><strong>{{.locale.Tr "admin.auths.attributes_in_bind"}}</strong></label>
Expand Down
25 changes: 25 additions & 0 deletions templates/admin/auth/source/saml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div class="saml field {{if not (eq .type 8)}}gt-hidden{{end}}">
<div class="required field">
<label for="saml_issuer">{{.locale.Tr "admin.auths.saml.issuer"}}</label>
<input id="saml_issuer" name="saml_issuer" value="{{.saml_issuer}}">
</div>
<div class="required field">
<label for="saml_login">{{.locale.Tr "admin.auths.saml.login"}}</label>
<input id="saml_login" name="saml_login" value="{{.saml_login}}">
</div>
<div class="optional field">
<label for="saml_logout">{{.locale.Tr "admin.auths.saml.logout"}}</label>
<input id="saml_logout" name="saml_logout" value="{{.saml_logout}}">
</div>
<div class="required field">
<label for="saml_certificate">{{.locale.Tr "admin.auths.saml.certificate"}}</label>
<textarea id="saml_certificate" name="saml_certificate" rows="5">{{.saml_certificate}}</textarea>
</div>
<div class="optional field">
<div class="ui checkbox">
<label for="skip_local_two_fa"><strong>{{.locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if .skip_local_two_fa}}checked{{end}}>
<p class="help">{{.locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
</div>
</div>
</div>
6 changes: 5 additions & 1 deletion web_src/js/features/admin/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function initAdminCommon() {
// New authentication
if ($('.admin.new.authentication').length > 0) {
$('#auth_type').on('change', function () {
hideElem($('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi'));
hideElem($('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi, .saml'));

$('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]').removeAttr('required');
$('.binddnrequired').removeClass('required');
Expand Down Expand Up @@ -138,6 +138,10 @@ export function initAdminCommon() {
showElem($('.sspi'));
$('.sspi div.required input').attr('required', 'required');
break;
case '8': // SAML
showElem($('.saml'));
$('.saml div.required input').attr('required', 'required');
break;
}
if (authType === '2' || authType === '5') {
onSecurityProtocolChange();
Expand Down