├── .gitignore ├── .travis.yml ├── README.md ├── access.go ├── access_test.go ├── app └── main.go ├── authz.go ├── client.go ├── code.go ├── doc.go ├── endpoint.go ├── errors.go ├── mongo.go ├── mongo_test.go ├── password.go ├── password_test.go ├── provider.go ├── provider_test.go ├── response.go ├── test.sh └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | services: 4 | - mongodb 5 | notificaitons: 6 | email: 7 | recipients: jason.mcvetta@gmail.com 8 | on_success: change 9 | on_failure: always 10 | before_script: 11 | - go get github.com/bmizerany/assert 12 | - go get github.com/jmcvetta/restclient 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | O2pro - OAuth2 Provider for Go 2 | ============================== 3 | 4 | # Deprecated 5 | 6 | `o2pro` is incomplete and deprecated. You may wish to check out 7 | [`fosite`](https://github.com/ory-am/fosite) instead. 8 | 9 | 10 | ----- 11 | 12 | 13 | Package `o2pro` is an [OAuth2](http://tools.ietf.org/html/rfc6749) provider for 14 | [Go](http://golang.org). 15 | 16 | O2pro is a work in progress. The following subset of the OAuth2 17 | specification is currently under development: 18 | 19 | * Resource Owner Password Credentials Grant: 20 | http://tools.ietf.org/html/rfc6749#section-4.3 21 | * Bearer Tokens: https://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16 22 | 23 | 24 | # Documentation 25 | 26 | See GoDoc for [API documentation](http://godoc.org/github.com/jmcvetta/o2pro). 27 | 28 | 29 | # Status 30 | 31 | Under development - not yet ready for use. 32 | 33 | [![Build Status](https://travis-ci.org/jmcvetta/o2pro.png)](https://travis-ci.org/jmcvetta/o2pro) 34 | [![Build Status](https://drone.io/github.com/jmcvetta/o2pro/status.png)](https://drone.io/github.com/jmcvetta/o2pro/latest) 35 | [![Coverage Status](https://coveralls.io/repos/jmcvetta/o2pro/badge.png?branch=master)](https://coveralls.io/r/jmcvetta/o2pro) 36 | 37 | 38 | # Contributing 39 | 40 | Contributions, in the form of Pull Requests or Issues, are gladly accepted. 41 | Before submitting a Pull Request, please ensure your code passes all tests, and 42 | that your changes do not decrease test coverage. I.e. if you add new features, 43 | also add corresponding new tests. 44 | 45 | 46 | # Sponsorship 47 | 48 | O2pro was originally written for a now defunct startup project. It is not 49 | currently under active development. [The 50 | author](mailto:jason.mcvetta@gmail.com) is seeking a company or companies 51 | interested in providing financial backing to enable its continued development. 52 | 53 | 54 | # License 55 | 56 | This is Free Software, released under the terms of the [GPL 57 | v3](http://www.gnu.org/copyleft/gpl.html). 58 | -------------------------------------------------------------------------------- /access.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | import "log" 8 | 9 | /* 10 | ACCESSING PROTECTED RESOURCES 11 | http://tools.ietf.org/html/rfc6749#section-7 12 | */ 13 | 14 | import "net/http" 15 | 16 | // RequireScope wraps a HandlerFunc, restricting access to authenticated users 17 | // with the specified scope. 18 | func (p *Provider) RequireScope(fn http.HandlerFunc, scope string) http.HandlerFunc { 19 | return func(w http.ResponseWriter, r *http.Request) { 20 | token, err := bearerToken(r) 21 | if err != nil { // No token found 22 | log.Println(err) 23 | http.Error(w, "", http.StatusUnauthorized) 24 | return 25 | } 26 | a, err := p.authz(token) 27 | if err != nil { 28 | log.Println(err) 29 | http.Error(w, "", http.StatusUnauthorized) 30 | return 31 | } 32 | _, ok := a.ScopesMap()[scope] 33 | if !ok { 34 | log.Printf("Need scope '%v' but only authorized for '%v'", scope, a.ScopeString()) 35 | http.Error(w, "", http.StatusUnauthorized) 36 | return 37 | } 38 | fn(w, r) // Call the wrapped function 39 | return 40 | } 41 | } 42 | 43 | // RequireAuthc wraps a HandlerFunc, restricting access to authenticated users. 44 | func (p *Provider) RequireAuthc(fn http.HandlerFunc) http.HandlerFunc { 45 | return func(w http.ResponseWriter, r *http.Request) { 46 | token, err := bearerToken(r) 47 | if err != nil { // No token found 48 | log.Println(err) 49 | http.Error(w, "", http.StatusUnauthorized) 50 | return 51 | } 52 | _, err = p.authz(token) 53 | if err != nil { 54 | log.Println(err) 55 | http.Error(w, "", http.StatusUnauthorized) 56 | return 57 | } 58 | fn(w, r) 59 | return 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /access_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | import ( 8 | "github.com/bmizerany/assert" 9 | "github.com/jmcvetta/restclient" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | ) 14 | 15 | func fooHandler(w http.ResponseWriter, r *http.Request) { 16 | w.WriteHeader(http.StatusOK) 17 | } 18 | 19 | func doTestRequireScope(p *Provider, t *testing.T) { 20 | h := p.RequireScope(fooHandler, "enterprise") 21 | hserv := httptest.NewServer(h) 22 | defer hserv.Close() 23 | // 24 | // Valid Scope 25 | // 26 | username := "jtkirk" 27 | scopes := []string{"enterprise", "shuttlecraft"} 28 | note := "foo bar baz" 29 | auth, _ := p.NewAuthz(username, note, scopes) 30 | header := make(http.Header) 31 | header.Add("Authorization", "Bearer "+auth.Token) 32 | rr := restclient.RequestResponse{ 33 | Url: hserv.URL, 34 | Method: "GET", 35 | Header: &header, 36 | } 37 | status, err := restclient.Do(&rr) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | assert.Equal(t, 200, status) 42 | // 43 | // No Token 44 | // 45 | rr = restclient.RequestResponse{ 46 | Url: hserv.URL, 47 | Method: "GET", 48 | } 49 | status, err = restclient.Do(&rr) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | assert.Equal(t, 401, status) 54 | // 55 | // Bad Header 56 | // 57 | header = make(http.Header) 58 | header.Add("Authorization", "foobar") 59 | rr = restclient.RequestResponse{ 60 | Url: hserv.URL, 61 | Method: "GET", 62 | Header: &header, 63 | } 64 | status, err = restclient.Do(&rr) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | assert.Equal(t, 401, status) 69 | // 70 | // Unauthorized Scope 71 | // 72 | h1 := p.RequireScope(fooHandler, "foobar") // Not among the authorized scopes 73 | hserv1 := httptest.NewServer(h1) 74 | defer hserv1.Close() 75 | header = make(http.Header) 76 | header.Add("Authorization", "Bearer "+auth.Token) 77 | rr = restclient.RequestResponse{ 78 | Url: hserv1.URL, 79 | Method: "GET", 80 | Header: &header, 81 | } 82 | status, err = restclient.Do(&rr) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | assert.Equal(t, 401, status) 87 | } 88 | 89 | func doTestRequireAuthc(p *Provider, t *testing.T) { 90 | h := p.RequireAuthc(fooHandler) 91 | hserv := httptest.NewServer(h) 92 | defer hserv.Close() 93 | // 94 | // Valid Authentication 95 | // 96 | auth, _ := p.NewAuthz("jtkirk", "", nil) 97 | header := make(http.Header) 98 | header.Add("Authorization", "Bearer "+auth.Token) 99 | rr := restclient.RequestResponse{ 100 | Url: hserv.URL, 101 | Method: "GET", 102 | Header: &header, 103 | } 104 | status, err := restclient.Do(&rr) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | assert.Equal(t, 200, status) 109 | // 110 | // Invalid Auth Token 111 | // 112 | header = make(http.Header) 113 | header.Add("Authorization", "Bearer foorbar") // "foobar" is not a valid token 114 | rr = restclient.RequestResponse{ 115 | Url: hserv.URL, 116 | Method: "GET", 117 | Header: &header, 118 | } 119 | status, err = restclient.Do(&rr) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | assert.Equal(t, 401, status) 124 | // 125 | // No Auth Token 126 | // 127 | rr = restclient.RequestResponse{ 128 | Url: hserv.URL, 129 | Method: "GET", 130 | } 131 | status, err = restclient.Do(&rr) 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | assert.Equal(t, 401, status) 136 | } 137 | -------------------------------------------------------------------------------- /app/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/darkhelmet/env" 9 | "github.com/jmcvetta/o2pro" 10 | "labix.org/v2/mgo" 11 | "log" 12 | "net/http" 13 | ) 14 | 15 | var ( 16 | addr = ":8080" 17 | ) 18 | 19 | // fakeAuth authorizes everyone for everything. 20 | func fakeAuth(username, password string, scopes []string) (bool, error) { 21 | return true, nil 22 | } 23 | 24 | func main() { 25 | log.SetFlags(log.Ltime | log.Lshortfile) 26 | // 27 | // Connect to MongoDB 28 | // 29 | mongoUrl := env.StringDefault("MONGOLAB_URI", "localhost") 30 | log.Println("Connecting to MongoDB on " + mongoUrl + "...") 31 | session, err := mgo.Dial(mongoUrl) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | defer session.Close() 36 | db := session.DB("") 37 | _, err = db.CollectionNames() 38 | if err != nil || db.Name == "test" { 39 | log.Println("Setting db name to 'o2pro'.") 40 | db = session.DB("o2pro") 41 | } 42 | a := o2pro.Authorizer(fakeAuth) 43 | srv, err := o2pro.NewMongoServer(db, "8h", a) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | hf := srv.PasswordHandler() 48 | http.HandleFunc("/auth", hf) 49 | log.Println("Listening on ", addr) 50 | log.Fatal(http.ListenAndServe(addr, nil)) 51 | } 52 | -------------------------------------------------------------------------------- /authz.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | import ( 8 | "code.google.com/p/go-uuid/uuid" 9 | "crypto/rand" 10 | "encoding/base64" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // An Authz is an authorization. 16 | type Authz struct { 17 | Id int64 `bson:",omitempty` 18 | Uuid string 19 | Token string 20 | User string 21 | ClientId int64 22 | Client *Client 23 | Issued time.Time 24 | Expiration time.Time 25 | Note string 26 | Scopes []string 27 | } 28 | 29 | // NewAuth issues a new authorization. 30 | func (p *Provider) NewAuthz(user, note string, scopes []string) (*Authz, error) { 31 | b := make([]byte, 16) 32 | _, err := rand.Read(b) 33 | if err != nil { 34 | return nil, err 35 | } 36 | token := uuid.New() + string(b) 37 | token = base64.StdEncoding.EncodeToString([]byte(token)) 38 | a := Authz{ 39 | Token: token, 40 | Uuid: uuid.New(), 41 | User: user, 42 | Scopes: scopes, 43 | Expiration: time.Now().Add(p.Duration), 44 | Note: note, 45 | } 46 | err = p.saveAuthz(&a) 47 | return &a, err 48 | } 49 | 50 | /* 51 | // SaveAuthz saves an authorization to storage. 52 | func (p *Provider) SaveAuthz(a *Authz) error { 53 | return p.saveAuthz(a) 54 | } 55 | */ 56 | 57 | // Authz looks up an authorization based on its token. 58 | func (p *Provider) Authz(token string) (*Authz, error) { 59 | return p.authz(token) 60 | } 61 | 62 | // ScopesMap returns a map of the scopes in this authorization, for easy look 63 | // up. Bool is always true. 64 | func (a *Authz) ScopesMap() map[string]bool { 65 | return sliceMap(a.Scopes) 66 | } 67 | 68 | func (a *Authz) ScopeString() string { 69 | return strings.Join(a.Scopes, " ") 70 | } 71 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | /* 8 | Client Credentials Grant 9 | https://tools.ietf.org/html/rfc6749#section-4.4 10 | */ 11 | 12 | import () 13 | 14 | const ( 15 | PublicClient = "public" 16 | ConfidentialClient = "confidential" 17 | ) 18 | 19 | // A Client is an application making protected resource requests on behalf of 20 | // the resource owner and with its authorization. 21 | type Client struct { 22 | Id int64 `bson:",omitempty` 23 | ClientType string // "public" or "confidential" 24 | RedirectUri string 25 | AppName string 26 | WebSite string 27 | Description string 28 | } 29 | -------------------------------------------------------------------------------- /code.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | /* 8 | Implementation of AUTHORIZATION CODE GRANT workflow 9 | https://tools.ietf.org/html/rfc6749#section-4.1 10 | */ 11 | 12 | import () 13 | 14 | // A Code is an authorization code, entitling its holder to be issued an 15 | // authorization. 16 | type Code struct { 17 | Id int64 `bson:",omitempty` 18 | } 19 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | /* 6 | Package o2pro is an OAuth2 provider. It currently implements only a subset of 7 | the full OAuth2 specification: 8 | 9 | - Resource Owner Password Credentials Grant: http://tools.ietf.org/html/rfc6749#section-4.3 10 | 11 | - Bearer Tokens: https://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16 12 | 13 | */ 14 | package o2pro 15 | -------------------------------------------------------------------------------- /endpoint.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | import ( 8 | "errors" 9 | ) 10 | 11 | // Standard Oauth2 error types 12 | // https://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16#section-3.1 13 | var ( 14 | // HTTP 400 15 | ErrInvalidRequest = errors.New("The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats the same parameter, uses more than one method for including an access token, or is otherwise malformed.") 16 | // HTTP 401 17 | ErrNotAuthorized = errors.New("Authorization not granted.") 18 | ErrInvalidToken = errors.New("The access token provided is expired, revoked, malformed, or invalid for other reasons.") 19 | // HTTP 403 20 | ErrInsufficientScope = errors.New("The request requires higher privileges than provided by the access token.") 21 | ) 22 | 23 | // Internal errors 24 | var ( 25 | ErrNotImplemented = errors.New("Not Implemented") 26 | ErrNoToken = errors.New("Request does not contain an access token.") 27 | ) 28 | 29 | type oauthError string 30 | 31 | // Error Codes 32 | const ( 33 | invalidRequest = oauthError("invalid_request") 34 | /* 35 | The request is missing a required parameter, includes an 36 | unsupported parameter value (other than grant type), 37 | repeats a parameter, includes multiple credentials, 38 | utilizes more than one mechanism for authenticating the 39 | client, or is otherwise malformed. 40 | */ 41 | 42 | invalidClient = "invalid_client" 43 | /* 44 | Client authentication failed (e.g., unknown client, no 45 | client authentication included, or unsupported 46 | authentication method). The authorization server MAY 47 | return an HTTP 401 (Unauthorized) status code to indicate 48 | which HTTP authentication schemes are supported. If the 49 | client attempted to authenticate via the "Authorization" 50 | request header field, the authorization server MUST 51 | respond with an HTTP 401 (Unauthorized) status code and 52 | include the "WWW-Authenticate" response header field 53 | matching the authentication scheme used by the client. 54 | */ 55 | 56 | invalidGrant = "invalid_grant" 57 | /* 58 | The provided authorization grant (e.g., authorization 59 | code, resource owner credentials) or refresh token is 60 | invalid, expired, revoked, does not match the redirection 61 | URI used in the authorization request, or was issued to 62 | another client. 63 | */ 64 | 65 | unauthorizedClient = "unauthorized_client" 66 | /* 67 | The authenticated client is not authorized to use this 68 | authorization grant type. 69 | */ 70 | 71 | unsupportedGrantType = "unsupported_grant_type" 72 | /* 73 | The authorization grant type is not supported by the 74 | authorization server. 75 | */ 76 | 77 | invalidScope = "invalid_scope" 78 | /* 79 | The requested scope is invalid, unknown, malformed, or 80 | exceeds the scope granted by the resource owner. 81 | */ 82 | ) 83 | 84 | // An ErrorResponse is sent with HTTP status code 400. 85 | type ErrorResponse struct { 86 | Error string `json:"error"` // REQUIRED. A single ASCII error code from the Error Codes constants. 87 | Desc string `json:"error_description,omitempty"` // OPTIONAL. Human-readable ASCII [USASCII] text providing additional information, used to assist the client developer in understanding the error that occurred. 88 | Uri string `json:"error_uri,omitempty"` // OPTIONAL. A URI identifying a human-readable web page with information about the error, used to provide the client developer with additional information about the error. 89 | 90 | } 91 | -------------------------------------------------------------------------------- /mongo.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | import ( 8 | "labix.org/v2/mgo" 9 | "time" 10 | ) 11 | 12 | // NewMongoStorage constructs a new mongoStorage. 13 | func NewMongoStorage(db *mgo.Database, dur time.Duration) Storage { 14 | return &mongoStorage{ 15 | db: db, 16 | name: "authorizations", 17 | expireAfter: dur, 18 | } 19 | } 20 | 21 | // mongoStorage implements Storage using MongoDB. 22 | type mongoStorage struct { 23 | db *mgo.Database 24 | name string // Collection name 25 | expireAfter time.Duration 26 | } 27 | 28 | func (m *mongoStorage) initialize() error { 29 | return m.migrate() 30 | } 31 | 32 | func (m *mongoStorage) migrate() error { 33 | // 34 | // Declare Indexes 35 | // 36 | idxs := []mgo.Index{ 37 | mgo.Index{ 38 | Key: []string{"token"}, 39 | Unique: true, 40 | DropDups: false, 41 | }, 42 | mgo.Index{ 43 | Key: []string{"expiration"}, 44 | Unique: true, 45 | DropDups: false, 46 | }, 47 | } 48 | c := m.col() 49 | for _, i := range idxs { 50 | err := c.EnsureIndex(i) 51 | if err != nil { 52 | return err 53 | } 54 | } 55 | return nil 56 | 57 | } 58 | 59 | func (s *mongoStorage) authz(token string) (*Authz, error) { 60 | a := new(Authz) 61 | c := s.col() 62 | query := struct { 63 | Token string 64 | }{ 65 | Token: token, 66 | } 67 | q := c.Find(query) 68 | cnt, err := q.Count() 69 | if err != nil { 70 | return a, err 71 | } 72 | // Token not found 73 | if cnt < 1 { 74 | return a, ErrInvalidToken 75 | } 76 | err = q.One(&a) 77 | if err != nil { 78 | return a, err 79 | } 80 | // Expired token 81 | if time.Now().After(a.Expiration) { 82 | c.Remove(query) 83 | return a, ErrInvalidToken 84 | } 85 | return a, nil 86 | } 87 | 88 | func (s *mongoStorage) saveAuthz(a *Authz) error { 89 | return s.col().Insert(a) 90 | } 91 | 92 | // col returns a Collection object in a new mgo session 93 | func (s *mongoStorage) col() *mgo.Collection { 94 | session := s.db.Session.Copy() 95 | d := session.DB(s.db.Name) 96 | return d.C(s.name) 97 | } 98 | -------------------------------------------------------------------------------- /mongo_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | import ( 8 | "github.com/bmizerany/assert" 9 | "labix.org/v2/mgo" 10 | "log" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func col(db *mgo.Database) *mgo.Collection { 16 | return db.C("authorizations") 17 | } 18 | 19 | func testMongo(t *testing.T) (*Provider, *mgo.Database) { 20 | log.SetFlags(log.Ltime | log.Lshortfile) 21 | session, err := mgo.Dial("mongodb://127.0.0.1") 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | db := session.DB("test_o2pro") 26 | dur, err := time.ParseDuration(DefaultExpireAfter) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | stor := NewMongoStorage(db, dur) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | p := NewProvider(stor, kirkAuthenticator, GrantAll) 35 | p.Scopes = testScopesAll 36 | p.DefaultScopes = testScopesDefault 37 | err = p.Initialize() 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | err = p.Migrate() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | return p, db 46 | } 47 | 48 | func TestMgoNewAuth(t *testing.T) { 49 | s, db := testMongo(t) 50 | username := "jtkirk" 51 | scopes := []string{"enterprise", "shuttlecraft"} 52 | note := "foo bar baz" 53 | auth, err := s.NewAuthz(username, note, scopes) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | c := col(db) 58 | query := struct { 59 | Token string 60 | }{ 61 | Token: auth.Token, 62 | } 63 | q := c.Find(&query) 64 | cnt, err := q.Count() 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | assert.Equal(t, 1, cnt) 69 | a := Authz{} 70 | err = q.One(&a) 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | assert.Equal(t, username, a.User) 75 | sm := a.ScopesMap() 76 | for _, scope := range scopes { 77 | _, ok := sm[scope] 78 | assert.T(t, ok, "Expected scope: ", scope) 79 | } 80 | } 81 | 82 | func TestMgoAuthz(t *testing.T) { 83 | p, _ := testMongo(t) 84 | doTestAuthz(p, t) 85 | } 86 | 87 | func TestMgoExpiration(t *testing.T) { 88 | p, _ := testMongo(t) 89 | doTestExpiration(p, t) 90 | } 91 | 92 | func TestMgoPasswordRequest(t *testing.T) { 93 | p, _ := testMongo(t) 94 | doTestPasswordRequest(p, t) 95 | } 96 | 97 | func TestMgoRequireScope(t *testing.T) { 98 | p, _ := testMongo(t) 99 | doTestRequireScope(p, t) 100 | } 101 | 102 | func TestMgoTestRequireAuthc(t *testing.T) { 103 | p, _ := testMongo(t) 104 | doTestRequireAuthc(p, t) 105 | } 106 | -------------------------------------------------------------------------------- /password.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | /* 8 | Implementation of RESOURCE OWNER PASSWORD CREDENTIALS GRANT workflow. 9 | http://tools.ietf.org/html/rfc6749#section-4.3 10 | */ 11 | 12 | import ( 13 | "encoding/json" 14 | "log" 15 | "net/http" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | // A PasswordRequest is submitted by a client requesting authorization using 21 | // the Resource Owner Password Credentials Grant flow. 22 | type PasswordRequest struct { 23 | GrantType string `json:"grant_type"` // REQUIRED. Value MUST be set to "password". 24 | Username string `json:"username"` // REQUIRED. The resource owner username. 25 | Password string `json:"password"` // REQUIRED. The resource owner password. 26 | Scope string `json:"scope"` // OPTIONAL. The scope of the access request as described by http://tools.ietf.org/html/rfc6749#section-3.3 27 | Note string `json:"note"` // OPTIONAL. Not part of RFC spec - inspired by Github. 28 | } 29 | 30 | // PasswordGrant supports authorization via the Resource Owner Password 31 | // Credentials Grant workflow. 32 | func passwordGrant(p *Provider, w http.ResponseWriter, r *http.Request) { 33 | // 34 | // Authenticate 35 | // 36 | malformed := "Malformed Authorization header" 37 | username, password, err := basicAuth(r) 38 | if err != nil { 39 | http.Error(w, malformed, http.StatusBadRequest) 40 | return 41 | } 42 | ok, err := p.Authenticate(username, password) 43 | if err != nil { 44 | log.Println(err) 45 | http.Error(w, "", http.StatusInternalServerError) 46 | return 47 | } 48 | if !ok { 49 | http.Error(w, "Invalid username/password", http.StatusUnauthorized) 50 | return 51 | } 52 | // 53 | // Parse authorization request 54 | // 55 | dec := json.NewDecoder(r.Body) 56 | var preq PasswordRequest 57 | err = dec.Decode(&preq) 58 | if err != nil && err.Error() != "EOF" { 59 | log.Println(err) 60 | msg := "Missing or bad request body" 61 | http.Error(w, msg, http.StatusBadRequest) 62 | return 63 | } 64 | if username != preq.Username || preq.GrantType != "password" { 65 | http.Error(w, "", http.StatusBadRequest) 66 | return 67 | } 68 | // 69 | // Validate scope 70 | // 71 | scopes := strings.Split(preq.Scope, " ") 72 | valid := sliceMap(p.Scopes) 73 | for _, scope := range scopes { 74 | _, ok = valid[scope] 75 | if !ok { 76 | http.Error(w, "Invalid scope: "+scope, http.StatusBadRequest) 77 | return 78 | } 79 | ok, err = p.Grant(username, scope, nil) 80 | if err != nil { 81 | log.Println(err) 82 | http.Error(w, "", http.StatusInternalServerError) 83 | return 84 | } 85 | if !ok { 86 | http.Error(w, "Not authorized for scope: "+scope, http.StatusUnauthorized) 87 | return 88 | } 89 | } 90 | // 91 | // Create new authorization 92 | // 93 | a, err := p.NewAuthz(preq.Username, preq.Note, scopes) 94 | if err != nil { 95 | log.Println(err) 96 | http.Error(w, "", http.StatusInternalServerError) 97 | return 98 | } 99 | // 100 | // Compose response 101 | // 102 | resp := TokenResponse{ 103 | AccessToken: a.Token, 104 | TokenType: "bearer", 105 | ExpiresIn: int(a.Expiration.Sub(time.Now()).Seconds()), 106 | Scope: a.ScopeString(), 107 | } 108 | enc := json.NewEncoder(w) 109 | enc.Encode(&resp) 110 | return 111 | } 112 | -------------------------------------------------------------------------------- /password_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | import ( 8 | "bytes" 9 | "code.google.com/p/go-uuid/uuid" 10 | "encoding/base64" 11 | "github.com/bmizerany/assert" 12 | "github.com/jmcvetta/restclient" 13 | "net/http" 14 | "net/http/httptest" 15 | "net/url" 16 | "strings" 17 | "testing" 18 | ) 19 | 20 | func doTestPasswordRequest(p *Provider, t *testing.T) { 21 | // 22 | // Prepare handler 23 | // 24 | hserv := httptest.NewServer(p.PasswordGrantHandler()) 25 | defer hserv.Close() 26 | // 27 | // REST request 28 | // 29 | scopes := []string{"enterprise", "intrepid"} 30 | scopeStr := strings.Join(scopes, " ") 31 | username := "jtkirk" 32 | password := "Beam me up, Scotty!" 33 | u := url.UserPassword(username, password) 34 | preq := PasswordRequest{ 35 | GrantType: "password", 36 | Username: "jtkirk", 37 | Password: password, 38 | Scope: scopeStr, 39 | Note: "foo bar baz", 40 | } 41 | var res TokenResponse 42 | var e interface{} 43 | rr := restclient.RequestResponse{ 44 | Url: hserv.URL, 45 | Method: "POST", 46 | Userinfo: u, 47 | Data: &preq, 48 | Result: &res, 49 | Error: &e, 50 | } 51 | c := restclient.New() 52 | c.UnsafeBasicAuth = true 53 | status, err := c.Do(&rr) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | assert.Equal(t, 200, status) 58 | assert.NotEqual(t, nil, uuid.Parse(res.AccessToken)) 59 | assert.Equal(t, scopeStr, res.Scope) 60 | assert.Equal(t, "bearer", res.TokenType) 61 | } 62 | 63 | func TestPasswordStorageErr(t *testing.T) { 64 | s := testNull(t) 65 | // 66 | // Prepare handler 67 | // 68 | hserv := httptest.NewServer(s.PasswordGrantHandler()) 69 | defer hserv.Close() 70 | // 71 | // REST request 72 | // 73 | scopes := []string{"enterprise", "intrepid"} 74 | scopeStr := strings.Join(scopes, " ") 75 | username := "jtkirk" 76 | password := "Beam me up, Scotty!" 77 | u := url.UserPassword(username, password) 78 | preq := PasswordRequest{ 79 | GrantType: "password", 80 | Username: "jtkirk", 81 | Password: password, 82 | Scope: scopeStr, 83 | } 84 | rr := restclient.RequestResponse{ 85 | Url: hserv.URL, 86 | Method: "POST", 87 | Userinfo: u, 88 | Data: &preq, 89 | } 90 | c := restclient.New() 91 | c.UnsafeBasicAuth = true 92 | status, err := c.Do(&rr) 93 | if err != nil { 94 | t.Error(err) 95 | } 96 | assert.Equal(t, 500, status) 97 | } 98 | 99 | func TestPasswordInvalidScope(t *testing.T) { 100 | s := testNull(t) 101 | // 102 | // Prepare handler 103 | // 104 | hserv := httptest.NewServer(s.PasswordGrantHandler()) 105 | defer hserv.Close() 106 | // 107 | // REST request 108 | // 109 | username := "jtkirk" 110 | password := "Beam me up, Scotty!" 111 | u := url.UserPassword(username, password) 112 | preq := PasswordRequest{ 113 | GrantType: "password", 114 | Username: "jtkirk", 115 | Password: password, 116 | Scope: "foobar", // invalid scope 117 | } 118 | rr := restclient.RequestResponse{ 119 | Url: hserv.URL, 120 | Method: "POST", 121 | Userinfo: u, 122 | Data: &preq, 123 | } 124 | c := restclient.New() 125 | c.UnsafeBasicAuth = true 126 | status, err := c.Do(&rr) 127 | if err != nil { 128 | t.Error(err) 129 | } 130 | assert.Equal(t, 400, status) 131 | } 132 | 133 | func TestPasswordAuthenticateErr(t *testing.T) { 134 | a := func(user, password string) (bool, error) { 135 | return false, ErrNotImplemented 136 | } 137 | s := NewProvider(&nullStorage{}, a, GrantAll) 138 | // 139 | // Prepare handler 140 | // 141 | hserv := httptest.NewServer(s.PasswordGrantHandler()) 142 | defer hserv.Close() 143 | // 144 | // REST request 145 | // 146 | username := "jtkirk" 147 | password := "Beam me up, Scotty!" 148 | u := url.UserPassword(username, password) 149 | rr := restclient.RequestResponse{ 150 | Url: hserv.URL, 151 | Method: "POST", 152 | Userinfo: u, 153 | } 154 | c := restclient.New() 155 | c.UnsafeBasicAuth = true 156 | status, err := c.Do(&rr) 157 | if err != nil { 158 | t.Error(err) 159 | } 160 | assert.Equal(t, 500, status) 161 | } 162 | 163 | func TestPasswordGrantErr(t *testing.T) { 164 | g := func(user, scope string, c *Client) (bool, error) { 165 | return false, ErrNotImplemented 166 | } 167 | s := NewProvider(&nullStorage{}, kirkAuthenticator, g) 168 | s.Scopes = testScopesAll 169 | s.DefaultScopes = testScopesDefault 170 | // 171 | // Prepare handler 172 | // 173 | hserv := httptest.NewServer(s.PasswordGrantHandler()) 174 | defer hserv.Close() 175 | // 176 | // REST request 177 | // 178 | username := "jtkirk" 179 | password := "Beam me up, Scotty!" 180 | scopes := []string{"enterprise", "intrepid"} 181 | scopeStr := strings.Join(scopes, " ") 182 | u := url.UserPassword(username, password) 183 | preq := PasswordRequest{ 184 | GrantType: "password", 185 | Username: "jtkirk", 186 | Password: password, 187 | Scope: scopeStr, 188 | } 189 | rr := restclient.RequestResponse{ 190 | Url: hserv.URL, 191 | Method: "POST", 192 | Userinfo: u, 193 | Data: &preq, 194 | } 195 | c := restclient.New() 196 | c.UnsafeBasicAuth = true 197 | status, err := c.Do(&rr) 198 | if err != nil { 199 | t.Error(err) 200 | } 201 | assert.Equal(t, 500, status) 202 | } 203 | 204 | func TestPasswordUnauthorizedScope(t *testing.T) { 205 | g := func(user, scope string, c *Client) (bool, error) { 206 | return false, nil 207 | } 208 | s := NewProvider(&nullStorage{}, kirkAuthenticator, g) 209 | s.Scopes = testScopesAll 210 | s.DefaultScopes = testScopesDefault 211 | // 212 | // Prepare handler 213 | // 214 | hserv := httptest.NewServer(s.PasswordGrantHandler()) 215 | defer hserv.Close() 216 | // 217 | // REST request 218 | // 219 | username := "jtkirk" 220 | password := "Beam me up, Scotty!" 221 | scopes := []string{"enterprise", "intrepid"} 222 | scopeStr := strings.Join(scopes, " ") 223 | u := url.UserPassword(username, password) 224 | preq := PasswordRequest{ 225 | GrantType: "password", 226 | Username: "jtkirk", 227 | Password: password, 228 | Scope: scopeStr, 229 | } 230 | rr := restclient.RequestResponse{ 231 | Url: hserv.URL, 232 | Method: "POST", 233 | Userinfo: u, 234 | Data: &preq, 235 | } 236 | c := restclient.New() 237 | c.UnsafeBasicAuth = true 238 | status, err := c.Do(&rr) 239 | if err != nil { 240 | t.Error(err) 241 | } 242 | assert.Equal(t, 401, status) 243 | } 244 | 245 | func TestPasswordBadCreds(t *testing.T) { 246 | s := testNull(t) 247 | // 248 | // Prepare handler 249 | // 250 | hserv := httptest.NewServer(s.PasswordGrantHandler()) 251 | defer hserv.Close() 252 | // 253 | // REST request 254 | // 255 | username := "jtkirk" 256 | password := "Go Klingons!" 257 | u := url.UserPassword(username, password) 258 | preq := PasswordRequest{ 259 | GrantType: "password", 260 | Username: "jtkirk", 261 | Password: password, 262 | } 263 | var res interface{} 264 | var e interface{} 265 | rr := restclient.RequestResponse{ 266 | Url: hserv.URL, 267 | Method: "POST", 268 | Userinfo: u, 269 | Data: &preq, 270 | Result: &res, 271 | Error: &e, 272 | } 273 | c := restclient.New() 274 | c.UnsafeBasicAuth = true 275 | status, err := c.Do(&rr) 276 | if err != nil { 277 | t.Fatal(err) 278 | } 279 | assert.Equal(t, 401, status) 280 | } 281 | 282 | func TestPasswordBadAuthHeader(t *testing.T) { 283 | s := testNull(t) 284 | // 285 | // Prepare handler 286 | // 287 | hserv := httptest.NewServer(s.PasswordGrantHandler()) 288 | defer hserv.Close() 289 | // 290 | // Regex doesn't match 291 | // 292 | req, err := http.NewRequest("POST", hserv.URL, nil) 293 | if err != nil { 294 | t.Fatal(err) 295 | } 296 | req.Header.Add("Authorization", "foobar") 297 | resp, err := http.DefaultClient.Do(req) 298 | if err != nil { 299 | t.Fatal(err) 300 | } 301 | assert.Equal(t, 400, resp.StatusCode) 302 | // 303 | // Base64 decode failed 304 | // 305 | req, err = http.NewRequest("POST", hserv.URL, nil) 306 | if err != nil { 307 | t.Fatal(err) 308 | } 309 | req.Header.Add("Authorization", "Basic foobar") 310 | resp, err = http.DefaultClient.Do(req) 311 | if err != nil { 312 | t.Fatal(err) 313 | } 314 | assert.Equal(t, 400, resp.StatusCode) 315 | // 316 | // String split failed 317 | // 318 | req, err = http.NewRequest("POST", hserv.URL, nil) 319 | if err != nil { 320 | t.Fatal(err) 321 | } 322 | str := base64.URLEncoding.EncodeToString([]byte("foobar")) 323 | req.Header.Add("Authorization", "Basic "+str) 324 | resp, err = http.DefaultClient.Do(req) 325 | if err != nil { 326 | t.Fatal(err) 327 | } 328 | assert.Equal(t, 400, resp.StatusCode) 329 | } 330 | 331 | func TestPasswordNoData(t *testing.T) { 332 | s := testNull(t) 333 | // 334 | // Prepare handler 335 | // 336 | hserv := httptest.NewServer(s.PasswordGrantHandler()) 337 | defer hserv.Close() 338 | // 339 | // REST request 340 | // 341 | username := "jtkirk" 342 | password := "Beam me up, Scotty!" 343 | u := url.UserPassword(username, password) 344 | rr := restclient.RequestResponse{ 345 | Url: hserv.URL, 346 | Method: "POST", 347 | Userinfo: u, 348 | } 349 | c := restclient.New() 350 | c.UnsafeBasicAuth = true 351 | status, err := c.Do(&rr) 352 | if err != nil { 353 | t.Fatal(err) 354 | } 355 | assert.Equal(t, 400, status) 356 | } 357 | 358 | func TestPasswordBogusData(t *testing.T) { 359 | s := testNull(t) 360 | // 361 | // Prepare handler 362 | // 363 | hserv := httptest.NewServer(s.PasswordGrantHandler()) 364 | defer hserv.Close() 365 | username := "jtkirk" 366 | password := "Beam me up, Scotty!" 367 | u := url.UserPassword(username, password) 368 | // 369 | // Valid JSON, bogus request 370 | // 371 | preq := PasswordRequest{ 372 | GrantType: "foobar", // Should be password 373 | Username: "jtkirk", 374 | Password: password, 375 | } 376 | rr := restclient.RequestResponse{ 377 | Url: hserv.URL, 378 | Method: "POST", 379 | Userinfo: u, 380 | Data: &preq, 381 | } 382 | c := restclient.New() 383 | c.UnsafeBasicAuth = true 384 | status, err := c.Do(&rr) 385 | if err != nil { 386 | t.Fatal(err) 387 | } 388 | assert.Equal(t, 400, status) 389 | } 390 | 391 | func TestPasswordBadlyFormed(t *testing.T) { 392 | s := testNull(t) 393 | // 394 | // Prepare handler 395 | // 396 | hserv := httptest.NewServer(s.PasswordGrantHandler()) 397 | defer hserv.Close() 398 | username := "jtkirk" 399 | password := "Beam me up, Scotty!" 400 | buf := bytes.NewBuffer([]byte("foobar")) 401 | req, err := http.NewRequest("POST", hserv.URL, buf) 402 | if err != nil { 403 | t.Fatal(err) 404 | } 405 | req.SetBasicAuth(username, password) 406 | resp, err := http.DefaultClient.Do(req) 407 | if err != nil { 408 | t.Fatal(err) 409 | } 410 | assert.Equal(t, 400, resp.StatusCode) 411 | 412 | } 413 | -------------------------------------------------------------------------------- /provider.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | import ( 8 | "log" 9 | "net/http" 10 | "os" 11 | "time" 12 | ) 13 | 14 | var ( 15 | DefaultExpireAfter = "8h" // Duration string for time.ParseDuration() 16 | DefaultLogger = log.New(os.Stdout, "[o2pro] ", log.Ltime|log.Ldate|log.Lshortfile) 17 | DefaultScopes = []string{"all"} 18 | ) 19 | 20 | // A Storage back end saves and retrieves authorizations to persistent storage. 21 | type Storage interface { 22 | saveAuthz(a *Authz) error 23 | authz(token string) (*Authz, error) 24 | initialize() error 25 | migrate() error 26 | } 27 | 28 | // An Authenticator authenticates a user's credentials. 29 | type Authenticator func(user, password string) (bool, error) 30 | 31 | // A Grantor decides whether to grant access for a given user, scope, and 32 | // client. Client is optional. 33 | type Grantor func(user, scope string, c *Client) (bool, error) 34 | 35 | // GrantAll is a Grantor that always returns true. 36 | func GrantAll(user, scope string, c *Client) (bool, error) { 37 | return true, nil 38 | } 39 | 40 | // NewProvider initializes a new OAuth2 provider server. 41 | func NewProvider(s Storage, a Authenticator, g Grantor) *Provider { 42 | dur, err := time.ParseDuration(DefaultExpireAfter) 43 | if err != nil { 44 | log.Panic(err) 45 | } 46 | return &Provider{ 47 | Storage: s, 48 | Scopes: DefaultScopes, 49 | DefaultScopes: DefaultScopes, 50 | Duration: dur, 51 | Logger: DefaultLogger, 52 | a: a, 53 | g: g, 54 | } 55 | } 56 | 57 | // A Provider is an OAuth2 authorization server. 58 | type Provider struct { 59 | Storage 60 | Scopes []string // All scopes supported by this server 61 | DefaultScopes []string // Issued if no specific scope(s) requested 62 | Duration time.Duration // Lifetime for an authorization 63 | Logger *log.Logger 64 | a Authenticator 65 | g Grantor 66 | } 67 | 68 | // Grant decides whether to grant an authorization. 69 | func (p *Provider) Grant(user, scope string, c *Client) (bool, error) { 70 | return p.g(user, scope, c) 71 | } 72 | 73 | // Authenticate validates a user's credentials. 74 | func (p *Provider) Authenticate(user, password string) (bool, error) { 75 | return p.a(user, password) 76 | } 77 | 78 | // Initialize prepares a fresh database, creating necessary schema, indexes, 79 | // etc. Behavior is undefined if called with an already-initialized db. 80 | func (p *Provider) Initialize() error { 81 | return p.initialize() 82 | } 83 | 84 | // Migrate attempts to update the database to use the latest schema, indexes, 85 | // etc. Some storage implementations may return ErrNotImplemented. 86 | func (p *Provider) Migrate() error { 87 | return p.migrate() 88 | } 89 | 90 | type handlerStub func(p *Provider, w http.ResponseWriter, r *http.Request) 91 | 92 | func (p *Provider) handlerFunc(hs handlerStub) http.HandlerFunc { 93 | return func(w http.ResponseWriter, r *http.Request) { 94 | hs(p, w, r) 95 | } 96 | } 97 | 98 | func (p *Provider) PasswordGrantHandler() http.HandlerFunc { 99 | return p.handlerFunc(passwordGrant) 100 | } 101 | -------------------------------------------------------------------------------- /provider_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | import ( 8 | "github.com/bmizerany/assert" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | var ( 14 | testScopesAll = []string{"enterprise", "shuttlecraft", "intrepid"} 15 | testScopesDefault = []string{"shuttlecraft"} 16 | ) 17 | 18 | // An Authenticator implementation that authenticates user "jtkirk" with 19 | // password "Beam me up, Scotty!". 20 | func kirkAuthenticator(username, password string) (bool, error) { 21 | if username == "jtkirk" && password == "Beam me up, Scotty!" { 22 | return true, nil 23 | } 24 | return false, nil 25 | } 26 | 27 | func doTestAuthz(p *Provider, t *testing.T) { 28 | username := "jtkirk" 29 | scopes := []string{"enterprise", "shuttlecraft"} 30 | auth, err := p.NewAuthz(username, "", scopes) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | a, err := p.Authz(auth.Token) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | assert.Equal(t, username, a.User) 39 | sm := a.ScopesMap() 40 | for _, scope := range scopes { 41 | _, ok := sm[scope] 42 | assert.T(t, ok, "Expected scope: ", scope) 43 | } 44 | } 45 | 46 | func doTestExpiration(p *Provider, t *testing.T) { 47 | five, _ := time.ParseDuration("5ms") 48 | seven, _ := time.ParseDuration("7ms") 49 | p.Duration = five 50 | username := "jtkirk" 51 | scopes := []string{"enterprise", "shuttlecraft"} 52 | auth, err := p.NewAuthz(username, "", scopes) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | time.Sleep(seven) // Authz should be expired 57 | _, err = p.Authz(auth.Token) 58 | if err != ErrInvalidToken { 59 | t.Fatal(err) 60 | } 61 | } 62 | 63 | // for testing things that do not depend on storage 64 | type nullStorage struct { 65 | } 66 | 67 | func (n *nullStorage) saveAuthz(a *Authz) error { 68 | return ErrNotImplemented 69 | } 70 | 71 | func (n *nullStorage) authz(token string) (*Authz, error) { 72 | return nil, ErrNotImplemented 73 | } 74 | 75 | func (n *nullStorage) initialize() error { 76 | return ErrNotImplemented 77 | } 78 | 79 | func (n *nullStorage) migrate() error { 80 | return ErrNotImplemented 81 | } 82 | 83 | // for testing things that do not depend on storage 84 | func testNull(t *testing.T) *Provider { 85 | p := NewProvider(&nullStorage{}, kirkAuthenticator, GrantAll) 86 | p.Scopes = testScopesAll 87 | p.DefaultScopes = testScopesDefault 88 | return p 89 | } 90 | 91 | func TestPrettyPrint(t *testing.T) { 92 | prettyPrint(testScopesAll) 93 | } 94 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | /* 8 | Implementation of https://tools.ietf.org/html/rfc6749#section-5.1 9 | */ 10 | 11 | // A TokenResponse is sent on a successful authorization request. 12 | type TokenResponse struct { 13 | AccessToken string `json:"access_token"` // REQUIRED. The access token issued by the authorization server. 14 | TokenType string `json:"token_type"` // REQUIRED. The type of the token issued as described in Section 7.1. Value is case insensitive. 15 | ExpiresIn int `json:"expires_in,omitempty"` // RECOMMENDED. The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will expire in one hour from the time the response was generated. If omitted, the authorization server SHOULD provide the expiration time via other means or document the default value. 16 | RefreshToken string `json:"refresh_token,omitempty"` // OPTIONAL. The refresh token, which can be used to obtain new access tokens using the same authorization grant as described in Section 6. 17 | Scope string `json:"scope"` // OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The scope of the access token as described by Section 3.3. 18 | } 19 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ( 4 | unset PGUSER 5 | unset PGPASSWORD 6 | unset PGDATABASE 7 | 8 | psql -c 'DROP DATABASE IF EXISTS o2pro_test;' 9 | psql -c 'CREATE DATABASE o2pro_test OWNER o2pro_test;' 10 | ) 11 | 12 | go test -v . 13 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Jason McVetta. This is Free Software, released under the 2 | // terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for details. 3 | // Resist intellectual serfdom - the ownership of ideas is akin to slavery. 4 | 5 | package o2pro 6 | 7 | import ( 8 | "encoding/base64" 9 | "encoding/json" 10 | "log" 11 | "net/http" 12 | "path/filepath" 13 | "regexp" 14 | "runtime" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | var ( 20 | basicRegex = regexp.MustCompile(`[Bb]asic (?P\S+)`) 21 | bearerRegex = regexp.MustCompile(`[Bb]earer (?P\S+)`) // Spec doesn't actually say "Bearer" should be case insensitive. 22 | ) 23 | 24 | // BasicAuth extracts username & password from an HTTP request's authorization 25 | // header. 26 | func basicAuth(r *http.Request) (username, password string, err error) { 27 | str := r.Header.Get("Authorization") 28 | matches := basicRegex.FindStringSubmatch(str) 29 | if len(matches) != 2 { 30 | log.Println("Regex doesn't match") 31 | err = ErrInvalidRequest 32 | return 33 | } 34 | encoded := matches[1] 35 | b, err := base64.URLEncoding.DecodeString(encoded) 36 | if err != nil { 37 | log.Println("Base64 decode failed") 38 | err = ErrInvalidRequest 39 | return 40 | } 41 | parts := strings.Split(string(b), ":") 42 | if len(parts) != 2 { 43 | log.Println("String split failed") 44 | err = ErrInvalidRequest 45 | return 46 | } 47 | username = parts[0] 48 | password = parts[1] 49 | return 50 | } 51 | 52 | // BearerToken extracts a bearer token from the authorization header, form 53 | // encoded body parameter, or URI query parameter of an HTTP request. 54 | func bearerToken(r *http.Request) (token string, err error) { 55 | // 56 | // Authorization Header 57 | // 58 | auth := r.Header.Get("Authorization") 59 | if auth != "" { 60 | matches := bearerRegex.FindStringSubmatch(auth) 61 | if len(matches) != 2 { 62 | log.Println("Regex doesn't match") 63 | log.Println("\t" + auth) 64 | err = ErrNoToken 65 | return 66 | } 67 | token = matches[1] 68 | return 69 | } 70 | // 71 | // Form-encoded Body Parameter 72 | // 73 | ct := r.Header.Get("Content-Type") 74 | if ct == "application/x-www-form-urlencoded" { 75 | type t struct { 76 | Token string `json:"access_token"` 77 | } 78 | s := new(t) 79 | dec := json.NewDecoder(r.Body) 80 | defer r.Body.Close() 81 | err = dec.Decode(s) 82 | token = s.Token 83 | return 84 | } 85 | token = r.URL.Query().Get("access_token") 86 | if token == "" { 87 | err = ErrNoToken 88 | } 89 | return 90 | } 91 | 92 | func sliceMap(s []string) map[string]bool { 93 | sm := make(map[string]bool, len(s)) 94 | for _, s := range s { 95 | sm[s] = true 96 | } 97 | return sm 98 | } 99 | 100 | func prettyPrint(v interface{}) { 101 | _, file, line, _ := runtime.Caller(1) 102 | lineNo := strconv.Itoa(line) 103 | file = filepath.Base(file) 104 | b, _ := json.MarshalIndent(v, "", "\t") 105 | s := file + ":" + lineNo + ": \n" + string(b) + "\n" 106 | println(s) 107 | } 108 | --------------------------------------------------------------------------------