Skip to content

Commit 7b4d2f7

Browse files
quasoftlafriks
authored andcommitted
Add single sign-on support via SSPI on Windows (#8463)
* Add single sign-on support via SSPI on Windows * Ensure plugins implement interface * Ensure plugins implement interface * Move functions used only by the SSPI auth method to sspi_windows.go * Field SSPISeparatorReplacement of AuthenticationForm should not be required via binding, as binding will insist the field is non-empty even if another login type is selected * Fix breaking of oauth authentication on download links. Do not create new session with SSPI authentication on download links. * Update documentation for the new 'SPNEGO with SSPI' login source * Mention in documentation that ROOT_URL should contain the FQDN of the server * Make sure that Contexter is not checking for active login sources when the ORM engine is not initialized (eg. when installing) * Always initialize and free SSO methods, even if they are not enabled, as a method can be activated while the app is running (from Authentication sources) * Add option in SSPIConfig for removing of domains from logon names * Update helper text for StripDomainNames option * Make sure handleSignIn() is called after a new user object is created by SSPI auth method * Remove default value from text of form field helper Co-Authored-By: Lauris BH <[email protected]> * Remove default value from text of form field helper Co-Authored-By: Lauris BH <[email protected]> * Remove default value from text of form field helper Co-Authored-By: Lauris BH <[email protected]> * Only make a query to the DB to check if SSPI is enabled on handlers that need that information for templates * Remove code duplication * Log errors in ActiveLoginSources Co-Authored-By: Lauris BH <[email protected]> * Revert suffix of randomly generated E-mails for Reverse proxy authentication Co-Authored-By: Lauris BH <[email protected]> * Revert unneeded white-space change in template Co-Authored-By: Lauris BH <[email protected]> * Add copyright comments at the top of new files * Use loopback name for randomly generated emails * Add locale tag for the SSPISeparatorReplacement field with proper casing * Revert casing of SSPISeparatorReplacement field in locale file, moving it up, next to other form fields * Update docs/content/doc/features/authentication.en-us.md Co-Authored-By: guillep2k <[email protected]> * Remove Priority() method and define the order in which SSO auth methods should be executed in one place * Log authenticated username only if it's not empty * Rephrase helper text for automatic creation of users * Return error if more than one active SSPI auth source is found * Change newUser() function to return error, letting caller log/handle the error * Move isPublicResource, isPublicPage and handleSignIn functions outside SSPI auth method to allow other SSO methods to reuse them if needed * Refactor initialization of the list containing SSO auth methods * Validate SSPI settings on POST * Change SSPI to only perform authentication on its own login page, API paths and download links. Leave Toggle middleware to redirect non authenticated users to login page * Make 'Default language' in SSPI config empty, unless changed by admin * Show error if admin tries to add a second authentication source of type SSPI * Simplify declaration of global variable * Rebuild gitgraph.js on Linux * Make sure config values containing only whitespace are not accepted
1 parent eb1b225 commit 7b4d2f7

File tree

174 files changed

+6362
-1305
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

174 files changed

+6362
-1305
lines changed

docs/content/doc/features/authentication.en-us.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,42 @@ configure this, set the fields below:
216216

217217
- Log in to Gitea as an Administrator and click on "Authentication" under Admin Panel.
218218
Then click `Add New Source` and fill in the details, changing all where appropriate.
219+
220+
## SPNEGO with SSPI (Kerberos/NTLM, for Windows only)
221+
222+
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.
223+
224+
Before activating SSPI single sign-on authentication (SSO) you have to prepare your environment:
225+
226+
- Create a separate user account in active directory, under which the `gitea.exe` process will be running (eg. `user` under domain `domain.local`):
227+
228+
- Create a service principal name for the host where `gitea.exe` is running with class `HTTP`:
229+
- Start `Command Prompt` or `PowerShell` as a priviledged domain user (eg. Domain Administrator)
230+
- 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:
231+
```
232+
setspn -A HTTP/host.domain.local domain\user
233+
```
234+
235+
- Sign in (*sign out if you were already signed in*) with the user created
236+
237+
- 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`)
238+
239+
- Start the web server (`gitea.exe web`)
240+
241+
- Enable SSPI authentication by adding an `SPNEGO with SSPI` authentication source in `Site Administration -> Authentication Sources`
242+
243+
- Sign in to a client computer in the same domain with any domain user (client computer, different from the server running `gitea.exe`)
244+
245+
- 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`)
246+
247+
- Start Chrome, Edge or Internet Explorer and navigate to the FQDN URL of gitea (eg. `http://host.domain.local:3000`)
248+
249+
- 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
250+
251+
- If it does not work, make sure that:
252+
- 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.
253+
- There is only one `HTTP/...` SPN for the host
254+
- The SPN contains only the hostname, without the port
255+
- You have added the URL of the web app to the `Local intranet zone`
256+
- The clocks of the server and client should not differ with more than 5 minutes (depends on group policy)
257+
- `Integrated Windows Authentication` should be enabled in Internet Explorer (under `Advanced settings`)

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ require (
7878
github.com/pquerna/otp v0.0.0-20160912161815-54653902c20e
7979
github.com/prometheus/client_golang v1.1.0
8080
github.com/prometheus/procfs v0.0.4 // indirect
81+
github.com/quasoft/websspi v1.0.0
8182
github.com/remyoudompheng/bigfft v0.0.0-20190321074620-2f0d2b0e0001 // indirect
8283
github.com/russross/blackfriday/v2 v2.0.1
8384
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
@@ -101,7 +102,7 @@ require (
101102
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f
102103
golang.org/x/net v0.0.0-20191101175033-0deb6923b6d9
103104
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
104-
golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b
105+
golang.org/x/sys v0.0.0-20191010194322-b09406accb47
105106
golang.org/x/text v0.3.2
106107
golang.org/x/tools v0.0.0-20190910221609-7f5965fd7709 // indirect
107108
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,8 @@ github.com/prometheus/procfs v0.0.4 h1:w8DjqFMJDjuVwdZBQoOozr4MVWOnwF7RcL/7uxBjY
459459
github.com/prometheus/procfs v0.0.4/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
460460
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
461461
github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4=
462+
github.com/quasoft/websspi v1.0.0 h1:5nDgdM5xSur9s+B5w2xQ5kxf5nUGqgFgU4W0aDLZ8Mw=
463+
github.com/quasoft/websspi v1.0.0/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
462464
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
463465
github.com/remyoudompheng/bigfft v0.0.0-20190321074620-2f0d2b0e0001 h1:YDeskXpkNDhPdWN3REluVa46HQOVuVkjkd2sWnrABNQ=
464466
github.com/remyoudompheng/bigfft v0.0.0-20190321074620-2f0d2b0e0001/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -656,8 +658,8 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
656658
golang.org/x/sys v0.0.0-20190730183949-1393eb018365/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
657659
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
658660
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
659-
golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b h1:3S2h5FadpNr0zUUCVZjlKIEYF+KaX/OBplTGo89CYHI=
660-
golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
661+
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
662+
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
661663
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
662664
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
663665
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=

models/login_source.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const (
3939
LoginPAM // 4
4040
LoginDLDAP // 5
4141
LoginOAuth2 // 6
42+
LoginSSPI // 7
4243
)
4344

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

5355
// SecurityProtocolNames contains the name of SecurityProtocol values.
@@ -63,6 +65,7 @@ var (
6365
_ core.Conversion = &SMTPConfig{}
6466
_ core.Conversion = &PAMConfig{}
6567
_ core.Conversion = &OAuth2Config{}
68+
_ core.Conversion = &SSPIConfig{}
6669
)
6770

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

146+
// SSPIConfig holds configuration for SSPI single sign-on.
147+
type SSPIConfig struct {
148+
AutoCreateUsers bool
149+
AutoActivateUsers bool
150+
StripDomainNames bool
151+
SeparatorReplacement string
152+
DefaultLanguage string
153+
}
154+
155+
// FromDB fills up an SSPIConfig from serialized format.
156+
func (cfg *SSPIConfig) FromDB(bs []byte) error {
157+
return json.Unmarshal(bs, cfg)
158+
}
159+
160+
// ToDB exports an SSPIConfig to a serialized format.
161+
func (cfg *SSPIConfig) ToDB() ([]byte, error) {
162+
return json.Marshal(cfg)
163+
}
164+
143165
// LoginSource represents an external way for authorizing users.
144166
type LoginSource struct {
145167
ID int64 `xorm:"pk autoincr"`
@@ -176,6 +198,8 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
176198
source.Cfg = new(PAMConfig)
177199
case LoginOAuth2:
178200
source.Cfg = new(OAuth2Config)
201+
case LoginSSPI:
202+
source.Cfg = new(SSPIConfig)
179203
default:
180204
panic("unrecognized login source type: " + com.ToStr(*val))
181205
}
@@ -212,6 +236,11 @@ func (source *LoginSource) IsOAuth2() bool {
212236
return source.Type == LoginOAuth2
213237
}
214238

239+
// IsSSPI returns true of this source is of the SSPI type.
240+
func (source *LoginSource) IsSSPI() bool {
241+
return source.Type == LoginSSPI
242+
}
243+
215244
// HasTLS returns true of this source supports TLS.
216245
func (source *LoginSource) HasTLS() bool {
217246
return ((source.IsLDAP() || source.IsDLDAP()) &&
@@ -264,6 +293,11 @@ func (source *LoginSource) OAuth2() *OAuth2Config {
264293
return source.Cfg.(*OAuth2Config)
265294
}
266295

296+
// SSPI returns SSPIConfig for this source, if of SSPI type.
297+
func (source *LoginSource) SSPI() *SSPIConfig {
298+
return source.Cfg.(*SSPIConfig)
299+
}
300+
267301
// CreateLoginSource inserts a LoginSource in the DB if not already
268302
// existing with the given name.
269303
func CreateLoginSource(source *LoginSource) error {
@@ -300,6 +334,38 @@ func LoginSources() ([]*LoginSource, error) {
300334
return auths, x.Find(&auths)
301335
}
302336

337+
// LoginSourcesByType returns all sources of the specified type
338+
func LoginSourcesByType(loginType LoginType) ([]*LoginSource, error) {
339+
sources := make([]*LoginSource, 0, 1)
340+
if err := x.Where("type = ?", loginType).Find(&sources); err != nil {
341+
return nil, err
342+
}
343+
return sources, nil
344+
}
345+
346+
// ActiveLoginSources returns all active sources of the specified type
347+
func ActiveLoginSources(loginType LoginType) ([]*LoginSource, error) {
348+
sources := make([]*LoginSource, 0, 1)
349+
if err := x.Where("is_actived = ? and type = ?", true, loginType).Find(&sources); err != nil {
350+
return nil, err
351+
}
352+
return sources, nil
353+
}
354+
355+
// IsSSPIEnabled returns true if there is at least one activated login
356+
// source of type LoginSSPI
357+
func IsSSPIEnabled() bool {
358+
if !HasEngine {
359+
return false
360+
}
361+
sources, err := ActiveLoginSources(LoginSSPI)
362+
if err != nil {
363+
log.Error("ActiveLoginSources: %v", err)
364+
return false
365+
}
366+
return len(sources) > 0
367+
}
368+
303369
// GetLoginSourceByID returns login source by given ID.
304370
func GetLoginSourceByID(id int64) (*LoginSource, error) {
305371
source := new(LoginSource)
@@ -719,8 +785,8 @@ func UserSignIn(username, password string) (*User, error) {
719785
}
720786

721787
for _, source := range sources {
722-
if source.IsOAuth2() {
723-
// don't try to authenticate against OAuth2 sources
788+
if source.IsOAuth2() || source.IsSSPI() {
789+
// don't try to authenticate against OAuth2 and SSPI sources here
724790
continue
725791
}
726792
authUser, err := ExternalUserLogin(nil, username, password, source, true)

0 commit comments

Comments
 (0)