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

Add single sign-on support via SSPI on Windows #8463

Merged
merged 41 commits into from
Nov 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
de972f1
Add single sign-on support via SSPI on Windows
quasoft Oct 22, 2019
a2d91cb
Ensure plugins implement interface
quasoft Oct 25, 2019
1a3aff3
Ensure plugins implement interface
quasoft Oct 25, 2019
f0668a3
Move functions used only by the SSPI auth method to sspi_windows.go
quasoft Oct 26, 2019
a75887e
Field SSPISeparatorReplacement of AuthenticationForm should not be re…
quasoft Oct 26, 2019
056929c
Fix breaking of oauth authentication on download links. Do not create…
quasoft Oct 27, 2019
c91d9f9
Update documentation for the new 'SPNEGO with SSPI' login source
quasoft Oct 27, 2019
aa43212
Mention in documentation that ROOT_URL should contain the FQDN of the…
quasoft Oct 27, 2019
e24ebb4
Make sure that Contexter is not checking for active login sources whe…
quasoft Oct 27, 2019
db8bbd8
Always initialize and free SSO methods, even if they are not enabled,…
quasoft Oct 27, 2019
62e38ab
Add option in SSPIConfig for removing of domains from logon names
quasoft Oct 28, 2019
5d99d6c
Update helper text for StripDomainNames option
quasoft Oct 28, 2019
d3c328f
Make sure handleSignIn() is called after a new user object is created…
quasoft Oct 28, 2019
16b47df
Remove default value from text of form field helper
quasoft Oct 28, 2019
d4cc173
Remove default value from text of form field helper
quasoft Oct 28, 2019
0d952c8
Remove default value from text of form field helper
quasoft Oct 28, 2019
9cbfd06
Only make a query to the DB to check if SSPI is enabled on handlers t…
quasoft Oct 28, 2019
2c3abb9
Remove code duplication
quasoft Oct 28, 2019
9bff4ae
Log errors in ActiveLoginSources
quasoft Oct 29, 2019
dd8937c
Revert suffix of randomly generated E-mails for Reverse proxy authent…
quasoft Oct 29, 2019
d5c3e9e
Revert unneeded white-space change in template
quasoft Oct 29, 2019
bbbe623
Add copyright comments at the top of new files
quasoft Oct 29, 2019
24c6ef1
Use loopback name for randomly generated emails
quasoft Oct 29, 2019
f7fb038
Add locale tag for the SSPISeparatorReplacement field with proper casing
quasoft Oct 29, 2019
c9a9f5b
Revert casing of SSPISeparatorReplacement field in locale file, movin…
quasoft Oct 29, 2019
0a4d2c3
Update docs/content/doc/features/authentication.en-us.md
quasoft Nov 18, 2019
734fb6d
Remove Priority() method and define the order in which SSO auth metho…
quasoft Nov 19, 2019
9d2a8dd
Log authenticated username only if it's not empty
quasoft Nov 19, 2019
a8503f0
Rephrase helper text for automatic creation of users
quasoft Nov 19, 2019
ab26413
Return error if more than one active SSPI auth source is found
quasoft Nov 19, 2019
f64128f
Change newUser() function to return error, letting caller log/handle …
quasoft Nov 20, 2019
d9018e9
Move isPublicResource, isPublicPage and handleSignIn functions outsid…
quasoft Nov 20, 2019
9432220
Refactor initialization of the list containing SSO auth methods
quasoft Nov 20, 2019
6fd308d
Validate SSPI settings on POST
quasoft Nov 20, 2019
be85ea7
Change SSPI to only perform authentication on its own login page, API…
quasoft Nov 21, 2019
e6dca6b
Make 'Default language' in SSPI config empty, unless changed by admin
quasoft Nov 21, 2019
29e47ef
Show error if admin tries to add a second authentication source of ty…
quasoft Nov 21, 2019
38be450
Simplify declaration of global variable
quasoft Nov 22, 2019
e4bbc91
Rebuild gitgraph.js on Linux
quasoft Nov 22, 2019
814873a
Make sure config values containing only whitespace are not accepted
quasoft Nov 22, 2019
359cd8e
Merge branch 'master' into sspi
lafriks Nov 22, 2019
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
39 changes: 39 additions & 0 deletions docs/content/doc/features/authentication.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,42 @@ configure this, set the fields below:

- Log in to Gitea as an Administrator and click on "Authentication" under Admin Panel.
Then click `Add New Source` and fill in the details, changing all where appropriate.

## SPNEGO with SSPI (Kerberos/NTLM, for Windows only)

Gitea supports SPNEGO single sign-on authentication (the scheme defined by RFC4559) for the web part of the server via the Security Support Provider Interface (SSPI) built in Windows. SSPI works only in Windows environments - when both the server and the clients are running Windows.

Before activating SSPI single sign-on authentication (SSO) you have to prepare your environment:

- Create a separate user account in active directory, under which the `gitea.exe` process will be running (eg. `user` under domain `domain.local`):

- Create a service principal name for the host where `gitea.exe` is running with class `HTTP`:
- Start `Command Prompt` or `PowerShell` as a priviledged domain user (eg. Domain Administrator)
- Run the command below, replacing `host.domain.local` with the fully qualified domain name (FQDN) of the server where the web application will be running, and `domain\user` with the name of the account created in the previous step:
```
setspn -A HTTP/host.domain.local domain\user
```

- Sign in (*sign out if you were already signed in*) with the user created

- Make sure that `ROOT_URL` in the `[server]` section of `custom/conf/app.ini` is the fully qualified domain name of the server where the web application will be running - the same you used when creating the service principal name (eg. `host.domain.local`)

- Start the web server (`gitea.exe web`)

- Enable SSPI authentication by adding an `SPNEGO with SSPI` authentication source in `Site Administration -> Authentication Sources`

- Sign in to a client computer in the same domain with any domain user (client computer, different from the server running `gitea.exe`)

- If you are using Chrome, Edge or Internet Explorer, add the URL of the web app to the Local intranet sites (`Internet Options -> Security -> Local intranet -> Sites`)

- Start Chrome, Edge or Internet Explorer and navigate to the FQDN URL of gitea (eg. `http://host.domain.local:3000`)

- Click the `Sign In` button on the dashboard and choose SSPI to be automatically logged in with the same user that is currently logged on to the computer

- If it does not work, make sure that:
- You are not running the web browser on the same server where gitea is running. You should be running the web browser on a domain joined computer (client) that is different from the server. If both the client and server are runnning on the same computer NTLM will be prefered over Kerberos.
- There is only one `HTTP/...` SPN for the host
- The SPN contains only the hostname, without the port
- You have added the URL of the web app to the `Local intranet zone`
- The clocks of the server and client should not differ with more than 5 minutes (depends on group policy)
- `Integrated Windows Authentication` should be enabled in Internet Explorer (under `Advanced settings`)
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ require (
github.com/pquerna/otp v0.0.0-20160912161815-54653902c20e
github.com/prometheus/client_golang v1.1.0
github.com/prometheus/procfs v0.0.4 // indirect
github.com/quasoft/websspi v1.0.0
github.com/remyoudompheng/bigfft v0.0.0-20190321074620-2f0d2b0e0001 // indirect
github.com/russross/blackfriday/v2 v2.0.1
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
Expand All @@ -101,7 +102,7 @@ require (
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f
golang.org/x/net v0.0.0-20191101175033-0deb6923b6d9
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b
golang.org/x/sys v0.0.0-20191010194322-b09406accb47
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20190910221609-7f5965fd7709 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,8 @@ github.com/prometheus/procfs v0.0.4 h1:w8DjqFMJDjuVwdZBQoOozr4MVWOnwF7RcL/7uxBjY
github.com/prometheus/procfs v0.0.4/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4=
github.com/quasoft/websspi v1.0.0 h1:5nDgdM5xSur9s+B5w2xQ5kxf5nUGqgFgU4W0aDLZ8Mw=
github.com/quasoft/websspi v1.0.0/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20190321074620-2f0d2b0e0001 h1:YDeskXpkNDhPdWN3REluVa46HQOVuVkjkd2sWnrABNQ=
github.com/remyoudompheng/bigfft v0.0.0-20190321074620-2f0d2b0e0001/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
Expand Down Expand Up @@ -656,8 +658,8 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190730183949-1393eb018365/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b h1:3S2h5FadpNr0zUUCVZjlKIEYF+KaX/OBplTGo89CYHI=
golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
Expand Down
70 changes: 68 additions & 2 deletions models/login_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const (
LoginPAM // 4
LoginDLDAP // 5
LoginOAuth2 // 6
LoginSSPI // 7
)

// LoginNames contains the name of LoginType values.
Expand All @@ -48,6 +49,7 @@ var LoginNames = map[LoginType]string{
LoginSMTP: "SMTP",
LoginPAM: "PAM",
LoginOAuth2: "OAuth2",
LoginSSPI: "SPNEGO with SSPI",
}

// SecurityProtocolNames contains the name of SecurityProtocol values.
Expand All @@ -63,6 +65,7 @@ var (
_ core.Conversion = &SMTPConfig{}
_ core.Conversion = &PAMConfig{}
_ core.Conversion = &OAuth2Config{}
_ core.Conversion = &SSPIConfig{}
)

// LDAPConfig holds configuration for LDAP login source.
Expand Down Expand Up @@ -140,6 +143,25 @@ func (cfg *OAuth2Config) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}

// SSPIConfig holds configuration for SSPI single sign-on.
type SSPIConfig struct {
AutoCreateUsers bool
AutoActivateUsers bool
StripDomainNames bool
SeparatorReplacement string
DefaultLanguage string
}

// FromDB fills up an SSPIConfig from serialized format.
func (cfg *SSPIConfig) FromDB(bs []byte) error {
return json.Unmarshal(bs, cfg)
}

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

// LoginSource represents an external way for authorizing users.
type LoginSource struct {
ID int64 `xorm:"pk autoincr"`
Expand Down Expand Up @@ -176,6 +198,8 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
source.Cfg = new(PAMConfig)
case LoginOAuth2:
source.Cfg = new(OAuth2Config)
case LoginSSPI:
source.Cfg = new(SSPIConfig)
default:
panic("unrecognized login source type: " + com.ToStr(*val))
}
Expand Down Expand Up @@ -212,6 +236,11 @@ func (source *LoginSource) IsOAuth2() bool {
return source.Type == LoginOAuth2
}

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

// HasTLS returns true of this source supports TLS.
func (source *LoginSource) HasTLS() bool {
return ((source.IsLDAP() || source.IsDLDAP()) &&
Expand Down Expand Up @@ -264,6 +293,11 @@ func (source *LoginSource) OAuth2() *OAuth2Config {
return source.Cfg.(*OAuth2Config)
}

// SSPI returns SSPIConfig for this source, if of SSPI type.
func (source *LoginSource) SSPI() *SSPIConfig {
return source.Cfg.(*SSPIConfig)
}

// CreateLoginSource inserts a LoginSource in the DB if not already
// existing with the given name.
func CreateLoginSource(source *LoginSource) error {
Expand Down Expand Up @@ -300,6 +334,38 @@ func LoginSources() ([]*LoginSource, error) {
return auths, x.Find(&auths)
}

// LoginSourcesByType returns all sources of the specified type
func LoginSourcesByType(loginType LoginType) ([]*LoginSource, error) {
sources := make([]*LoginSource, 0, 1)
if err := x.Where("type = ?", loginType).Find(&sources); err != nil {
return nil, err
}
return sources, nil
}

// ActiveLoginSources returns all active sources of the specified type
func ActiveLoginSources(loginType LoginType) ([]*LoginSource, error) {
sources := make([]*LoginSource, 0, 1)
if err := x.Where("is_actived = ? and type = ?", true, loginType).Find(&sources); err != nil {
return nil, err
}
return sources, nil
}

// IsSSPIEnabled returns true if there is at least one activated login
// source of type LoginSSPI
func IsSSPIEnabled() bool {
if !HasEngine {
return false
}
sources, err := ActiveLoginSources(LoginSSPI)
if err != nil {
log.Error("ActiveLoginSources: %v", err)
return false
quasoft marked this conversation as resolved.
Show resolved Hide resolved
}
return len(sources) > 0
}

// GetLoginSourceByID returns login source by given ID.
func GetLoginSourceByID(id int64) (*LoginSource, error) {
source := new(LoginSource)
Expand Down Expand Up @@ -719,8 +785,8 @@ func UserSignIn(username, password string) (*User, error) {
}

for _, source := range sources {
if source.IsOAuth2() {
// don't try to authenticate against OAuth2 sources
if source.IsOAuth2() || source.IsSSPI() {
// don't try to authenticate against OAuth2 and SSPI sources here
continue
}
authUser, err := ExternalUserLogin(nil, username, password, source, true)
Expand Down
Loading