pax_global_header00006660000000000000000000000064134635501460014521gustar00rootroot0000000000000052 comment=21fec4f7b063bde90bdd8229ea124a5615a37a35 golang-github-abbot-go-http-auth-0.4.0/000077500000000000000000000000001346355014600176555ustar00rootroot00000000000000golang-github-abbot-go-http-auth-0.4.0/.gitignore000066400000000000000000000000361346355014600216440ustar00rootroot00000000000000*~ *.a *.6 *.out _testmain.go golang-github-abbot-go-http-auth-0.4.0/LICENSE000066400000000000000000000236771346355014600207010ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS golang-github-abbot-go-http-auth-0.4.0/Makefile000066400000000000000000000002371346355014600213170ustar00rootroot00000000000000include $(GOROOT)/src/Make.inc TARG=auth_digest GOFILES=\ auth.go\ digest.go\ basic.go\ misc.go\ md5crypt.go\ users.go\ include $(GOROOT)/src/Make.pkg golang-github-abbot-go-http-auth-0.4.0/README.md000066400000000000000000000042621346355014600211400ustar00rootroot00000000000000HTTP Authentication implementation in Go ======================================== This is an implementation of HTTP Basic and HTTP Digest authentication in Go language. It is designed as a simple wrapper for http.RequestHandler functions. Features -------- * Supports HTTP Basic and HTTP Digest authentication. * Supports htpasswd and htdigest formatted files. * Automatic reloading of password files. * Pluggable interface for user/password storage. * Supports MD5, SHA1 and BCrypt for Basic authentication password storage. * Configurable Digest nonce cache size with expiration. * Wrapper for legacy http handlers (http.HandlerFunc interface) Example usage ------------- This is a complete working example for Basic auth: package main import ( "fmt" "net/http" auth "github.com/abbot/go-http-auth" ) func Secret(user, realm string) string { if user == "john" { // password is "hello" return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1" } return "" } func handle(w http.ResponseWriter, r *auth.AuthenticatedRequest) { fmt.Fprintf(w, "

Hello, %s!

", r.Username) } func main() { authenticator := auth.NewBasicAuthenticator("example.com", Secret) http.HandleFunc("/", authenticator.Wrap(handle)) http.ListenAndServe(":8080", nil) } See more examples in the "examples" directory. Legal ----- This module is developed under Apache 2.0 license, and can be used for open and proprietary projects. Copyright 2012-2013 Lev Shamardin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file or any other part of this project except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. golang-github-abbot-go-http-auth-0.4.0/auth.go000066400000000000000000000061301346355014600211450ustar00rootroot00000000000000// Package auth is an implementation of HTTP Basic and HTTP Digest authentication. package auth import ( "net/http" "golang.org/x/net/context" ) /* Request handlers must take AuthenticatedRequest instead of http.Request */ type AuthenticatedRequest struct { http.Request /* Authenticated user name. Current API implies that Username is never empty, which means that authentication is always done before calling the request handler. */ Username string } /* AuthenticatedHandlerFunc is like http.HandlerFunc, but takes AuthenticatedRequest instead of http.Request */ type AuthenticatedHandlerFunc func(http.ResponseWriter, *AuthenticatedRequest) /* Authenticator wraps an AuthenticatedHandlerFunc with authentication-checking code. Typical Authenticator usage is something like: authenticator := SomeAuthenticator(...) http.HandleFunc("/", authenticator(my_handler)) Authenticator wrapper checks the user authentication and calls the wrapped function only after authentication has succeeded. Otherwise, it returns a handler which initiates the authentication procedure. */ type Authenticator func(AuthenticatedHandlerFunc) http.HandlerFunc // Info contains authentication information for the request. type Info struct { // Authenticated is set to true when request was authenticated // successfully, i.e. username and password passed in request did // pass the check. Authenticated bool // Username contains a user name passed in the request when // Authenticated is true. It's value is undefined if Authenticated // is false. Username string // ResponseHeaders contains extra headers that must be set by server // when sending back HTTP response. ResponseHeaders http.Header } // UpdateHeaders updates headers with this Info's ResponseHeaders. It is // safe to call this function on nil Info. func (i *Info) UpdateHeaders(headers http.Header) { if i == nil { return } for k, values := range i.ResponseHeaders { for _, v := range values { headers.Add(k, v) } } } type key int // used for context keys var infoKey key = 0 type AuthenticatorInterface interface { // NewContext returns a new context carrying authentication // information extracted from the request. NewContext(ctx context.Context, r *http.Request) context.Context // Wrap returns an http.HandlerFunc which wraps // AuthenticatedHandlerFunc with this authenticator's // authentication checks. Wrap(AuthenticatedHandlerFunc) http.HandlerFunc } // FromContext returns authentication information from the context or // nil if no such information present. func FromContext(ctx context.Context) *Info { info, ok := ctx.Value(infoKey).(*Info) if !ok { return nil } return info } // AuthUsernameHeader is the header set by JustCheck functions. It // contains an authenticated username (if authentication was // successful). const AuthUsernameHeader = "X-Authenticated-Username" func JustCheck(auth AuthenticatorInterface, wrapped http.HandlerFunc) http.HandlerFunc { return auth.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) { ar.Header.Set(AuthUsernameHeader, ar.Username) wrapped(w, &ar.Request) }) } golang-github-abbot-go-http-auth-0.4.0/basic.go000066400000000000000000000120711346355014600212660ustar00rootroot00000000000000package auth import ( "bytes" "crypto/sha1" "crypto/subtle" "encoding/base64" "errors" "net/http" "strings" "golang.org/x/crypto/bcrypt" "golang.org/x/net/context" ) type compareFunc func(hashedPassword, password []byte) error var ( errMismatchedHashAndPassword = errors.New("mismatched hash and password") compareFuncs = []struct { prefix string compare compareFunc }{ {"", compareMD5HashAndPassword}, // default compareFunc {"{SHA}", compareShaHashAndPassword}, // Bcrypt is complicated. According to crypt(3) from // crypt_blowfish version 1.3 (fetched from // http://www.openwall.com/crypt/crypt_blowfish-1.3.tar.gz), there // are three different has prefixes: "$2a$", used by versions up // to 1.0.4, and "$2x$" and "$2y$", used in all later // versions. "$2a$" has a known bug, "$2x$" was added as a // migration path for systems with "$2a$" prefix and still has a // bug, and only "$2y$" should be used by modern systems. The bug // has something to do with handling of 8-bit characters. Since // both "$2a$" and "$2x$" are deprecated, we are handling them the // same way as "$2y$", which will yield correct results for 7-bit // character passwords, but is wrong for 8-bit character // passwords. You have to upgrade to "$2y$" if you want sant 8-bit // character password support with bcrypt. To add to the mess, // OpenBSD 5.5. introduced "$2b$" prefix, which behaves exactly // like "$2y$" according to the same source. {"$2a$", bcrypt.CompareHashAndPassword}, {"$2b$", bcrypt.CompareHashAndPassword}, {"$2x$", bcrypt.CompareHashAndPassword}, {"$2y$", bcrypt.CompareHashAndPassword}, } ) type BasicAuth struct { Realm string Secrets SecretProvider // Headers used by authenticator. Set to ProxyHeaders to use with // proxy server. When nil, NormalHeaders are used. Headers *Headers } // check that BasicAuth implements AuthenticatorInterface var _ = (AuthenticatorInterface)((*BasicAuth)(nil)) /* Checks the username/password combination from the request. Returns either an empty string (authentication failed) or the name of the authenticated user. Supports MD5 and SHA1 password entries */ func (a *BasicAuth) CheckAuth(r *http.Request) string { s := strings.SplitN(r.Header.Get(a.Headers.V().Authorization), " ", 2) if len(s) != 2 || s[0] != "Basic" { return "" } b, err := base64.StdEncoding.DecodeString(s[1]) if err != nil { return "" } pair := strings.SplitN(string(b), ":", 2) if len(pair) != 2 { return "" } user, password := pair[0], pair[1] secret := a.Secrets(user, a.Realm) if secret == "" { return "" } compare := compareFuncs[0].compare for _, cmp := range compareFuncs[1:] { if strings.HasPrefix(secret, cmp.prefix) { compare = cmp.compare break } } if compare([]byte(secret), []byte(password)) != nil { return "" } return pair[0] } func compareShaHashAndPassword(hashedPassword, password []byte) error { d := sha1.New() d.Write(password) if subtle.ConstantTimeCompare(hashedPassword[5:], []byte(base64.StdEncoding.EncodeToString(d.Sum(nil)))) != 1 { return errMismatchedHashAndPassword } return nil } func compareMD5HashAndPassword(hashedPassword, password []byte) error { parts := bytes.SplitN(hashedPassword, []byte("$"), 4) if len(parts) != 4 { return errMismatchedHashAndPassword } magic := []byte("$" + string(parts[1]) + "$") salt := parts[2] if subtle.ConstantTimeCompare(hashedPassword, MD5Crypt(password, salt, magic)) != 1 { return errMismatchedHashAndPassword } return nil } /* http.Handler for BasicAuth which initiates the authentication process (or requires reauthentication). */ func (a *BasicAuth) RequireAuth(w http.ResponseWriter, r *http.Request) { w.Header().Set(contentType, a.Headers.V().UnauthContentType) w.Header().Set(a.Headers.V().Authenticate, `Basic realm="`+a.Realm+`"`) w.WriteHeader(a.Headers.V().UnauthCode) w.Write([]byte(a.Headers.V().UnauthResponse)) } /* BasicAuthenticator returns a function, which wraps an AuthenticatedHandlerFunc converting it to http.HandlerFunc. This wrapper function checks the authentication and either sends back required authentication headers, or calls the wrapped function with authenticated username in the AuthenticatedRequest. */ func (a *BasicAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if username := a.CheckAuth(r); username == "" { a.RequireAuth(w, r) } else { ar := &AuthenticatedRequest{Request: *r, Username: username} wrapped(w, ar) } } } // NewContext returns a context carrying authentication information for the request. func (a *BasicAuth) NewContext(ctx context.Context, r *http.Request) context.Context { info := &Info{Username: a.CheckAuth(r), ResponseHeaders: make(http.Header)} info.Authenticated = (info.Username != "") if !info.Authenticated { info.ResponseHeaders.Set(a.Headers.V().Authenticate, `Basic realm="`+a.Realm+`"`) } return context.WithValue(ctx, infoKey, info) } func NewBasicAuthenticator(realm string, secrets SecretProvider) *BasicAuth { return &BasicAuth{Realm: realm, Secrets: secrets} } golang-github-abbot-go-http-auth-0.4.0/basic_test.go000066400000000000000000000017351346355014600223320ustar00rootroot00000000000000package auth import ( "encoding/base64" "net/http" "testing" ) func TestAuthBasic(t *testing.T) { secrets := HtpasswdFileProvider("test.htpasswd") a := &BasicAuth{Realm: "example.com", Secrets: secrets} r := &http.Request{} r.Method = "GET" if a.CheckAuth(r) != "" { t.Fatal("CheckAuth passed on empty headers") } r.Header = http.Header(make(map[string][]string)) r.Header.Set("Authorization", "Digest blabla ololo") if a.CheckAuth(r) != "" { t.Fatal("CheckAuth passed on bad headers") } r.Header.Set("Authorization", "Basic !@#") if a.CheckAuth(r) != "" { t.Fatal("CheckAuth passed on bad base64 data") } data := [][]string{ {"test", "hello"}, {"test2", "hello2"}, {"test3", "hello3"}, {"test16", "topsecret"}, } for _, tc := range data { auth := base64.StdEncoding.EncodeToString([]byte(tc[0] + ":" + tc[1])) r.Header.Set("Authorization", "Basic "+auth) if a.CheckAuth(r) != tc[0] { t.Fatalf("CheckAuth failed for user '%s'", tc[0]) } } } golang-github-abbot-go-http-auth-0.4.0/digest.go000066400000000000000000000200341346355014600214620ustar00rootroot00000000000000package auth import ( "crypto/subtle" "fmt" "net/http" "net/url" "sort" "strconv" "strings" "sync" "time" "golang.org/x/net/context" ) type digest_client struct { nc uint64 last_seen int64 } type DigestAuth struct { Realm string Opaque string Secrets SecretProvider PlainTextSecrets bool IgnoreNonceCount bool // Headers used by authenticator. Set to ProxyHeaders to use with // proxy server. When nil, NormalHeaders are used. Headers *Headers /* Approximate size of Client's Cache. When actual number of tracked client nonces exceeds ClientCacheSize+ClientCacheTolerance, ClientCacheTolerance*2 older entries are purged. */ ClientCacheSize int ClientCacheTolerance int clients map[string]*digest_client mutex sync.Mutex } // check that DigestAuth implements AuthenticatorInterface var _ = (AuthenticatorInterface)((*DigestAuth)(nil)) type digest_cache_entry struct { nonce string last_seen int64 } type digest_cache []digest_cache_entry func (c digest_cache) Less(i, j int) bool { return c[i].last_seen < c[j].last_seen } func (c digest_cache) Len() int { return len(c) } func (c digest_cache) Swap(i, j int) { c[i], c[j] = c[j], c[i] } /* Remove count oldest entries from DigestAuth.clients */ func (a *DigestAuth) Purge(count int) { entries := make([]digest_cache_entry, 0, len(a.clients)) for nonce, client := range a.clients { entries = append(entries, digest_cache_entry{nonce, client.last_seen}) } cache := digest_cache(entries) sort.Sort(cache) for _, client := range cache[:count] { delete(a.clients, client.nonce) } } /* http.Handler for DigestAuth which initiates the authentication process (or requires reauthentication). */ func (a *DigestAuth) RequireAuth(w http.ResponseWriter, r *http.Request) { if len(a.clients) > a.ClientCacheSize+a.ClientCacheTolerance { a.Purge(a.ClientCacheTolerance * 2) } nonce := RandomKey() a.clients[nonce] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} w.Header().Set(contentType, a.Headers.V().UnauthContentType) w.Header().Set(a.Headers.V().Authenticate, fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm="MD5", qop="auth"`, a.Realm, nonce, a.Opaque)) w.WriteHeader(a.Headers.V().UnauthCode) w.Write([]byte(a.Headers.V().UnauthResponse)) } /* Parse Authorization header from the http.Request. Returns a map of auth parameters or nil if the header is not a valid parsable Digest auth header. */ func DigestAuthParams(authorization string) map[string]string { s := strings.SplitN(authorization, " ", 2) if len(s) != 2 || s[0] != "Digest" { return nil } return ParsePairs(s[1]) } /* Check if request contains valid authentication data. Returns a pair of username, authinfo where username is the name of the authenticated user or an empty string and authinfo is the contents for the optional Authentication-Info response header. */ func (da *DigestAuth) CheckAuth(r *http.Request) (username string, authinfo *string) { da.mutex.Lock() defer da.mutex.Unlock() username = "" authinfo = nil auth := DigestAuthParams(r.Header.Get(da.Headers.V().Authorization)) if auth == nil { return "", nil } // RFC2617 Section 3.2.1 specifies that unset value of algorithm in // WWW-Authenticate Response header should be treated as // "MD5". According to section 3.2.2 the "algorithm" value in // subsequent Request Authorization header must be set to whatever // was supplied in the WWW-Authenticate Response header. This // implementation always returns an algorithm in WWW-Authenticate // header, however there seems to be broken clients in the wild // which do not set the algorithm. Assume the unset algorithm in // Authorization header to be equal to MD5. if _, ok := auth["algorithm"]; !ok { auth["algorithm"] = "MD5" } if da.Opaque != auth["opaque"] || auth["algorithm"] != "MD5" || auth["qop"] != "auth" { return "", nil } // Check if the requested URI matches auth header if r.RequestURI != auth["uri"] { // We allow auth["uri"] to be a full path prefix of request-uri // for some reason lost in history, which is probably wrong, but // used to be like that for quite some time // (https://tools.ietf.org/html/rfc2617#section-3.2.2 explicitly // says that auth["uri"] is the request-uri). // // TODO: make an option to allow only strict checking. switch u, err := url.Parse(auth["uri"]); { case err != nil: return "", nil case r.URL == nil: return "", nil case len(u.Path) > len(r.URL.Path): return "", nil case !strings.HasPrefix(r.URL.Path, u.Path): return "", nil } } HA1 := da.Secrets(auth["username"], da.Realm) if da.PlainTextSecrets { HA1 = H(auth["username"] + ":" + da.Realm + ":" + HA1) } HA2 := H(r.Method + ":" + auth["uri"]) KD := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], HA2}, ":")) if subtle.ConstantTimeCompare([]byte(KD), []byte(auth["response"])) != 1 { return "", nil } // At this point crypto checks are completed and validated. // Now check if the session is valid. nc, err := strconv.ParseUint(auth["nc"], 16, 64) if err != nil { return "", nil } if client, ok := da.clients[auth["nonce"]]; !ok { return "", nil } else { if client.nc != 0 && client.nc >= nc && !da.IgnoreNonceCount { return "", nil } client.nc = nc client.last_seen = time.Now().UnixNano() } resp_HA2 := H(":" + auth["uri"]) rspauth := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], resp_HA2}, ":")) info := fmt.Sprintf(`qop="auth", rspauth="%s", cnonce="%s", nc="%s"`, rspauth, auth["cnonce"], auth["nc"]) return auth["username"], &info } /* Default values for ClientCacheSize and ClientCacheTolerance for DigestAuth */ const DefaultClientCacheSize = 1000 const DefaultClientCacheTolerance = 100 /* Wrap returns an Authenticator which uses HTTP Digest authentication. Arguments: realm: The authentication realm. secrets: SecretProvider which must return HA1 digests for the same realm as above. */ func (a *DigestAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if username, authinfo := a.CheckAuth(r); username == "" { a.RequireAuth(w, r) } else { ar := &AuthenticatedRequest{Request: *r, Username: username} if authinfo != nil { w.Header().Set(a.Headers.V().AuthInfo, *authinfo) } wrapped(w, ar) } } } /* JustCheck returns function which converts an http.HandlerFunc into a http.HandlerFunc which requires authentication. Username is passed as an extra X-Authenticated-Username header. */ func (a *DigestAuth) JustCheck(wrapped http.HandlerFunc) http.HandlerFunc { return a.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) { ar.Header.Set(AuthUsernameHeader, ar.Username) wrapped(w, &ar.Request) }) } // NewContext returns a context carrying authentication information for the request. func (a *DigestAuth) NewContext(ctx context.Context, r *http.Request) context.Context { username, authinfo := a.CheckAuth(r) info := &Info{Username: username, ResponseHeaders: make(http.Header)} if username != "" { info.Authenticated = true info.ResponseHeaders.Set(a.Headers.V().AuthInfo, *authinfo) } else { // return back digest WWW-Authenticate header if len(a.clients) > a.ClientCacheSize+a.ClientCacheTolerance { a.Purge(a.ClientCacheTolerance * 2) } nonce := RandomKey() a.clients[nonce] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} info.ResponseHeaders.Set(a.Headers.V().Authenticate, fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm="MD5", qop="auth"`, a.Realm, nonce, a.Opaque)) } return context.WithValue(ctx, infoKey, info) } func NewDigestAuthenticator(realm string, secrets SecretProvider) *DigestAuth { da := &DigestAuth{ Opaque: RandomKey(), Realm: realm, Secrets: secrets, PlainTextSecrets: false, ClientCacheSize: DefaultClientCacheSize, ClientCacheTolerance: DefaultClientCacheTolerance, clients: map[string]*digest_client{}} return da } golang-github-abbot-go-http-auth-0.4.0/digest_test.go000066400000000000000000000056051346355014600225300ustar00rootroot00000000000000package auth import ( "net/http" "net/url" "testing" "time" ) func TestAuthDigest(t *testing.T) { secrets := HtdigestFileProvider("test.htdigest") da := &DigestAuth{Opaque: "U7H+ier3Ae8Skd/g", Realm: "example.com", Secrets: secrets, clients: map[string]*digest_client{}} r := &http.Request{} r.Method = "GET" if u, _ := da.CheckAuth(r); u != "" { t.Fatal("non-empty auth for empty request header") } r.Header = http.Header(make(map[string][]string)) r.Header.Set("Authorization", "Digest blabla") if u, _ := da.CheckAuth(r); u != "" { t.Fatal("non-empty auth for bad request header") } r.Header.Set("Authorization", `Digest username="test", realm="example.com", nonce="Vb9BP/h81n3GpTTB", uri="/t2", cnonce="NjE4MTM2", nc=00000001, qop="auth", response="ffc357c4eba74773c8687e0bc724c9a3", opaque="U7H+ier3Ae8Skd/g", algorithm="MD5"`) if u, _ := da.CheckAuth(r); u != "" { t.Fatal("non-empty auth for unknown client") } r.URL, _ = url.Parse("/t2") da.clients["Vb9BP/h81n3GpTTB"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} if u, _ := da.CheckAuth(r); u != "test" { t.Fatal("empty auth for legitimate client") } // our nc is now 0, client nc is 1 if u, _ := da.CheckAuth(r); u != "" { t.Fatal("non-empty auth for outdated nc") } // try again with nc checking off da.IgnoreNonceCount = true if u, _ := da.CheckAuth(r); u != "test" { t.Fatal("empty auth for outdated nc even though nc checking is off") } da.IgnoreNonceCount = false r.URL, _ = url.Parse("/") da.clients["Vb9BP/h81n3GpTTB"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} if u, _ := da.CheckAuth(r); u != "" { t.Fatal("non-empty auth for bad request path") } r.URL, _ = url.Parse("/t3") da.clients["Vb9BP/h81n3GpTTB"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} if u, _ := da.CheckAuth(r); u != "" { t.Fatal("non-empty auth for bad request path") } da.clients["+RbVXSbIoa1SaJk1"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} r.Header.Set("Authorization", `Digest username="test", realm="example.com", nonce="+RbVXSbIoa1SaJk1", uri="/", cnonce="NjE4NDkw", nc=00000001, qop="auth", response="c08918024d7faaabd5424654c4e3ad1c", opaque="U7H+ier3Ae8Skd/g", algorithm="MD5"`) if u, _ := da.CheckAuth(r); u != "test" { t.Fatal("empty auth for valid request in subpath") } } func TestDigestAuthParams(t *testing.T) { const authorization = `Digest username="test", realm="", nonce="FRPnGdb8lvM1UHhi", uri="/css?family=Source+Sans+Pro:400,700,400italic,700italic|Source+Code+Pro", algorithm=MD5, response="fdcdd78e5b306ffed343d0ec3967f2e5", opaque="lEgVjogmIar2fg/t", qop=auth, nc=00000001, cnonce="e76b05db27a3b323"` params := DigestAuthParams(authorization) want := "/css?family=Source+Sans+Pro:400,700,400italic,700italic|Source+Code+Pro" if params["uri"] != want { t.Fatalf("failed to parse uri with embedded commas, got %q want %q", params["uri"], want) } } golang-github-abbot-go-http-auth-0.4.0/examples/000077500000000000000000000000001346355014600214735ustar00rootroot00000000000000golang-github-abbot-go-http-auth-0.4.0/examples/basic.go000066400000000000000000000011531346355014600231030ustar00rootroot00000000000000// +build ignore /* Example application using Basic auth Build with: go build basic.go */ package main import ( auth ".." "fmt" "net/http" ) func Secret(user, realm string) string { if user == "john" { // password is "hello" return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1" } return "" } func handle(w http.ResponseWriter, r *auth.AuthenticatedRequest) { fmt.Fprintf(w, "

Hello, %s!

", r.Username) } func main() { authenticator := auth.NewBasicAuthenticator("example.com", Secret) http.HandleFunc("/", authenticator.Wrap(handle)) http.ListenAndServe(":8080", nil) } golang-github-abbot-go-http-auth-0.4.0/examples/context.go000066400000000000000000000027111346355014600235070ustar00rootroot00000000000000// +build ignore /* Example application using NewContext/FromContext Build with: go build context.go */ package main import ( "fmt" "net/http" auth ".." "golang.org/x/net/context" ) func Secret(user, realm string) string { if user == "john" { // password is "hello" return "b98e16cbc3d01734b264adba7baa3bf9" } return "" } type ContextHandler interface { ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) } type ContextHandlerFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request) func (f ContextHandlerFunc) ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) { f(ctx, w, r) } func handle(ctx context.Context, w http.ResponseWriter, r *http.Request) { authInfo := auth.FromContext(ctx) authInfo.UpdateHeaders(w.Header()) if authInfo == nil || !authInfo.Authenticated { http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } fmt.Fprintf(w, "

Hello, %s!

", authInfo.Username) } func authenticatedHandler(a auth.AuthenticatorInterface, h ContextHandler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := a.NewContext(context.Background(), r) h.ServeHTTP(ctx, w, r) }) } func main() { authenticator := auth.NewDigestAuthenticator("example.com", Secret) http.Handle("/", authenticatedHandler(authenticator, ContextHandlerFunc(handle))) http.ListenAndServe(":8080", nil) } golang-github-abbot-go-http-auth-0.4.0/examples/digest.go000066400000000000000000000011541346355014600233020ustar00rootroot00000000000000// +build ignore /* Example application using Digest auth Build with: go build digest.go */ package main import ( auth ".." "fmt" "net/http" ) func Secret(user, realm string) string { if user == "john" { // password is "hello" return "b98e16cbc3d01734b264adba7baa3bf9" } return "" } func handle(w http.ResponseWriter, r *auth.AuthenticatedRequest) { fmt.Fprintf(w, "

Hello, %s!

", r.Username) } func main() { authenticator := auth.NewDigestAuthenticator("example.com", Secret) http.HandleFunc("/", authenticator.Wrap(handle)) http.ListenAndServe(":8080", nil) } golang-github-abbot-go-http-auth-0.4.0/examples/wrapped.go000066400000000000000000000013611346355014600234650ustar00rootroot00000000000000// +build ignore /* Example demonstrating how to wrap an application which is unaware of authenticated requests with a "pass-through" authentication Build with: go build wrapped.go */ package main import ( auth ".." "fmt" "net/http" ) func Secret(user, realm string) string { if user == "john" { // password is "hello" return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1" } return "" } func regular_handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "

This application is unaware of authentication

") } func main() { authenticator := auth.NewBasicAuthenticator("example.com", Secret) http.HandleFunc("/", auth.JustCheck(authenticator, regular_handler)) http.ListenAndServe(":8080", nil) } golang-github-abbot-go-http-auth-0.4.0/md5crypt.go000066400000000000000000000031361346355014600217560ustar00rootroot00000000000000package auth import "crypto/md5" import "strings" const itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" var md5_crypt_swaps = [16]int{12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11} type MD5Entry struct { Magic, Salt, Hash []byte } func NewMD5Entry(e string) *MD5Entry { parts := strings.SplitN(e, "$", 4) if len(parts) != 4 { return nil } return &MD5Entry{ Magic: []byte("$" + parts[1] + "$"), Salt: []byte(parts[2]), Hash: []byte(parts[3]), } } /* MD5 password crypt implementation */ func MD5Crypt(password, salt, magic []byte) []byte { d := md5.New() d.Write(password) d.Write(magic) d.Write(salt) d2 := md5.New() d2.Write(password) d2.Write(salt) d2.Write(password) for i, mixin := 0, d2.Sum(nil); i < len(password); i++ { d.Write([]byte{mixin[i%16]}) } for i := len(password); i != 0; i >>= 1 { if i&1 == 0 { d.Write([]byte{password[0]}) } else { d.Write([]byte{0}) } } final := d.Sum(nil) for i := 0; i < 1000; i++ { d2 := md5.New() if i&1 == 0 { d2.Write(final) } else { d2.Write(password) } if i%3 != 0 { d2.Write(salt) } if i%7 != 0 { d2.Write(password) } if i&1 == 0 { d2.Write(password) } else { d2.Write(final) } final = d2.Sum(nil) } result := make([]byte, 0, 22) v := uint(0) bits := uint(0) for _, i := range md5_crypt_swaps { v |= (uint(final[i]) << bits) for bits = bits + 8; bits > 6; bits -= 6 { result = append(result, itoa64[v&0x3f]) v >>= 6 } } result = append(result, itoa64[v&0x3f]) return append(append(append(magic, salt...), '$'), result...) } golang-github-abbot-go-http-auth-0.4.0/md5crypt_test.go000066400000000000000000000010621346355014600230110ustar00rootroot00000000000000package auth import "testing" func Test_MD5Crypt(t *testing.T) { test_cases := [][]string{ {"apache", "$apr1$J.w5a/..$IW9y6DR0oO/ADuhlMF5/X1"}, {"pass", "$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90"}, {"topsecret", "$apr1$JI4wh3am$AmhephVqLTUyAVpFQeHZC0"}, } for _, tc := range test_cases { e := NewMD5Entry(tc[1]) result := MD5Crypt([]byte(tc[0]), e.Salt, e.Magic) if string(result) != tc[1] { t.Fatalf("MD5Crypt returned '%s' instead of '%s'", string(result), tc[1]) } t.Logf("MD5Crypt: '%s' (%s%s$) -> %s", tc[0], e.Magic, e.Salt, result) } } golang-github-abbot-go-http-auth-0.4.0/misc.go000066400000000000000000000072251346355014600211450ustar00rootroot00000000000000package auth import ( "bytes" "crypto/md5" "crypto/rand" "encoding/base64" "fmt" "net/http" "strings" ) // RandomKey returns a random 16-byte base64 alphabet string func RandomKey() string { k := make([]byte, 12) for bytes := 0; bytes < len(k); { n, err := rand.Read(k[bytes:]) if err != nil { panic("rand.Read() failed") } bytes += n } return base64.StdEncoding.EncodeToString(k) } // H function for MD5 algorithm (returns a lower-case hex MD5 digest) func H(data string) string { digest := md5.New() digest.Write([]byte(data)) return fmt.Sprintf("%x", digest.Sum(nil)) } // ParseList parses a comma-separated list of values as described by // RFC 2068 and returns list elements. // // Lifted from https://code.google.com/p/gorilla/source/browse/http/parser/parser.go // which was ported from urllib2.parse_http_list, from the Python // standard library. func ParseList(value string) []string { var list []string var escape, quote bool b := new(bytes.Buffer) for _, r := range value { switch { case escape: b.WriteRune(r) escape = false case quote: if r == '\\' { escape = true } else { if r == '"' { quote = false } b.WriteRune(r) } case r == ',': list = append(list, strings.TrimSpace(b.String())) b.Reset() case r == '"': quote = true b.WriteRune(r) default: b.WriteRune(r) } } // Append last part. if s := b.String(); s != "" { list = append(list, strings.TrimSpace(s)) } return list } // ParsePairs extracts key/value pairs from a comma-separated list of // values as described by RFC 2068 and returns a map[key]value. The // resulting values are unquoted. If a list element doesn't contain a // "=", the key is the element itself and the value is an empty // string. // // Lifted from https://code.google.com/p/gorilla/source/browse/http/parser/parser.go func ParsePairs(value string) map[string]string { m := make(map[string]string) for _, pair := range ParseList(strings.TrimSpace(value)) { if i := strings.Index(pair, "="); i < 0 { m[pair] = "" } else { v := pair[i+1:] if v[0] == '"' && v[len(v)-1] == '"' { // Unquote it. v = v[1 : len(v)-1] } m[pair[:i]] = v } } return m } // Headers contains header and error codes used by authenticator. type Headers struct { Authenticate string // WWW-Authenticate Authorization string // Authorization AuthInfo string // Authentication-Info UnauthCode int // 401 UnauthContentType string // text/plain UnauthResponse string // Unauthorized. } // V returns NormalHeaders when h is nil, or h otherwise. Allows to // use uninitialized *Headers values in structs. func (h *Headers) V() *Headers { if h == nil { return NormalHeaders } return h } var ( // NormalHeaders are the regular Headers used by an HTTP Server for // request authentication. NormalHeaders = &Headers{ Authenticate: "WWW-Authenticate", Authorization: "Authorization", AuthInfo: "Authentication-Info", UnauthCode: http.StatusUnauthorized, UnauthContentType: "text/plain", UnauthResponse: fmt.Sprintf("%d %s\n", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)), } // ProxyHeaders are Headers used by an HTTP Proxy server for proxy // access authentication. ProxyHeaders = &Headers{ Authenticate: "Proxy-Authenticate", Authorization: "Proxy-Authorization", AuthInfo: "Proxy-Authentication-Info", UnauthCode: http.StatusProxyAuthRequired, UnauthContentType: "text/plain", UnauthResponse: fmt.Sprintf("%d %s\n", http.StatusProxyAuthRequired, http.StatusText(http.StatusProxyAuthRequired)), } ) const contentType = "Content-Type" golang-github-abbot-go-http-auth-0.4.0/misc_test.go000066400000000000000000000022351346355014600222000ustar00rootroot00000000000000package auth import ( "reflect" "testing" ) func TestH(t *testing.T) { const hello = "Hello, world!" const hello_md5 = "6cd3556deb0da54bca060b4c39479839" h := H(hello) if h != hello_md5 { t.Fatal("Incorrect digest for test string:", h, "instead of", hello_md5) } } func TestParsePairs(t *testing.T) { const header = `username="\test", realm="a \"quoted\" string", nonce="FRPnGdb8lvM1UHhi", uri="/css?family=Source+Sans+Pro:400,700,400italic,700italic|Source+Code+Pro", algorithm=MD5, response="fdcdd78e5b306ffed343d0ec3967f2e5", opaque="lEgVjogmIar2fg/t", qop=auth, nc=00000001, cnonce="e76b05db27a3b323"` want := map[string]string{ "username": "test", "realm": `a "quoted" string`, "nonce": "FRPnGdb8lvM1UHhi", "uri": "/css?family=Source+Sans+Pro:400,700,400italic,700italic|Source+Code+Pro", "algorithm": "MD5", "response": "fdcdd78e5b306ffed343d0ec3967f2e5", "opaque": "lEgVjogmIar2fg/t", "qop": "auth", "nc": "00000001", "cnonce": "e76b05db27a3b323", } got := ParsePairs(header) if !reflect.DeepEqual(got, want) { t.Fatalf("failed to correctly parse pairs, got %v, want %v\ndiff: %s", got, want) } } golang-github-abbot-go-http-auth-0.4.0/test.htdigest000066400000000000000000000000621346355014600223670ustar00rootroot00000000000000test:example.com:aa78524fceb0e50fd8ca96dd818b8cf9 golang-github-abbot-go-http-auth-0.4.0/test.htpasswd000066400000000000000000000003031346355014600224070ustar00rootroot00000000000000test:{SHA}qvTGHdzF6KLavt4PO0gs2a6pQ00= test2:$apr1$a0j62R97$mYqFkloXH0/UOaUnAiV2b0 test16:$apr1$JI4wh3am$AmhephVqLTUyAVpFQeHZC0 test3:$2y$05$ih3C91zUBSTFcAh2mQnZYuob0UOZVEf16wl/ukgjDhjvj.xgM1WwS golang-github-abbot-go-http-auth-0.4.0/users.go000066400000000000000000000063521346355014600213530ustar00rootroot00000000000000package auth import ( "encoding/csv" "os" "sync" ) /* SecretProvider is used by authenticators. Takes user name and realm as an argument, returns secret required for authentication (HA1 for digest authentication, properly encrypted password for basic). Returning an empty string means failing the authentication. */ type SecretProvider func(user, realm string) string /* Common functions for file auto-reloading */ type File struct { Path string Info os.FileInfo /* must be set in inherited types during initialization */ Reload func() mu sync.Mutex } func (f *File) ReloadIfNeeded() { info, err := os.Stat(f.Path) if err != nil { panic(err) } f.mu.Lock() defer f.mu.Unlock() if f.Info == nil || f.Info.ModTime() != info.ModTime() { f.Info = info f.Reload() } } /* Structure used for htdigest file authentication. Users map realms to maps of users to their HA1 digests. */ type HtdigestFile struct { File Users map[string]map[string]string mu sync.RWMutex } func reload_htdigest(hf *HtdigestFile) { r, err := os.Open(hf.Path) if err != nil { panic(err) } csv_reader := csv.NewReader(r) csv_reader.Comma = ':' csv_reader.Comment = '#' csv_reader.TrimLeadingSpace = true records, err := csv_reader.ReadAll() if err != nil { panic(err) } hf.mu.Lock() defer hf.mu.Unlock() hf.Users = make(map[string]map[string]string) for _, record := range records { _, exists := hf.Users[record[1]] if !exists { hf.Users[record[1]] = make(map[string]string) } hf.Users[record[1]][record[0]] = record[2] } } /* SecretProvider implementation based on htdigest-formated files. Will reload htdigest file on changes. Will panic on syntax errors in htdigest files. */ func HtdigestFileProvider(filename string) SecretProvider { hf := &HtdigestFile{File: File{Path: filename}} hf.Reload = func() { reload_htdigest(hf) } return func(user, realm string) string { hf.ReloadIfNeeded() hf.mu.RLock() defer hf.mu.RUnlock() _, exists := hf.Users[realm] if !exists { return "" } digest, exists := hf.Users[realm][user] if !exists { return "" } return digest } } /* Structure used for htdigest file authentication. Users map users to their salted encrypted password */ type HtpasswdFile struct { File Users map[string]string mu sync.RWMutex } func reload_htpasswd(h *HtpasswdFile) { r, err := os.Open(h.Path) if err != nil { panic(err) } csv_reader := csv.NewReader(r) csv_reader.Comma = ':' csv_reader.Comment = '#' csv_reader.TrimLeadingSpace = true records, err := csv_reader.ReadAll() if err != nil { panic(err) } h.mu.Lock() defer h.mu.Unlock() h.Users = make(map[string]string) for _, record := range records { h.Users[record[0]] = record[1] } } /* SecretProvider implementation based on htpasswd-formated files. Will reload htpasswd file on changes. Will panic on syntax errors in htpasswd files. Realm argument of the SecretProvider is ignored. */ func HtpasswdFileProvider(filename string) SecretProvider { h := &HtpasswdFile{File: File{Path: filename}} h.Reload = func() { reload_htpasswd(h) } return func(user, realm string) string { h.ReloadIfNeeded() h.mu.RLock() password, exists := h.Users[user] h.mu.RUnlock() if !exists { return "" } return password } } golang-github-abbot-go-http-auth-0.4.0/users_test.go000066400000000000000000000022261346355014600224060ustar00rootroot00000000000000package auth import ( "os" "testing" "time" ) func TestHtdigestFile(t *testing.T) { secrets := HtdigestFileProvider("test.htdigest") digest := secrets("test", "example.com") if digest != "aa78524fceb0e50fd8ca96dd818b8cf9" { t.Fatal("Incorrect digest for test user:", digest) } digest = secrets("test", "example1.com") if digest != "" { t.Fatal("Got digest for user in non-existant realm:", digest) } digest = secrets("test1", "example.com") if digest != "" { t.Fatal("Got digest for non-existant user:", digest) } } func TestHtpasswdFile(t *testing.T) { secrets := HtpasswdFileProvider("test.htpasswd") passwd := secrets("test", "blah") if passwd != "{SHA}qvTGHdzF6KLavt4PO0gs2a6pQ00=" { t.Fatal("Incorrect passwd for test user:", passwd) } passwd = secrets("nosuchuser", "blah") if passwd != "" { t.Fatal("Got passwd for non-existant user:", passwd) } } // TestConcurrent verifies potential race condition in users reading logic func TestConcurrent(t *testing.T) { secrets := HtpasswdFileProvider("test.htpasswd") os.Chtimes("test.htpasswd", time.Now(), time.Now()) go func() { secrets("test", "blah") }() secrets("test", "blah") }