auth
84:4cb65cf90217 Browse Files
Start supporting our pluggable grant_type. Define GrantType as a way to bundle information that can be used to validate requests based on their grant_type parameter. Move our validation of the authorization_code grant_type out of GetTokenHandler and into its own function. Define RegisterGrantType as a way to register new grant_type bundles and associate them with the string passed to grant_type. This enables other packages to define RegisterGrantType in their init() functions and plug in new grant types without forking this code. Implement RegisterGrantType for our authorization_code grant type.
1.1 --- a/grant.go Sat Dec 06 02:03:20 2014 -0500 1.2 +++ b/grant.go Sat Dec 06 03:33:11 2014 -0500 1.3 @@ -1,12 +1,21 @@ 1.4 package auth 1.5 1.6 import ( 1.7 + "encoding/json" 1.8 "errors" 1.9 + "net/http" 1.10 "time" 1.11 1.12 "code.secondbit.org/uuid" 1.13 ) 1.14 1.15 +func init() { 1.16 + RegisterGrantType("authorization_code", GrantType{ 1.17 + Validate: authCodeGrantValidate, 1.18 + IssuesRefresh: true, 1.19 + }) 1.20 +} 1.21 + 1.22 var ( 1.23 // ErrNoGrantStore is returned when a Context tries to act on a grantStore without setting one first. 1.24 ErrNoGrantStore = errors.New("no grantStore was specified for the Context") 1.25 @@ -67,3 +76,69 @@ 1.26 delete(m.grants, code) 1.27 return nil 1.28 } 1.29 + 1.30 +func authCodeGrantValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) { 1.31 + enc := json.NewEncoder(w) 1.32 + code := r.PostFormValue("code") 1.33 + if code == "" { 1.34 + w.WriteHeader(http.StatusBadRequest) 1.35 + renderJSONError(enc, "invalid_request") 1.36 + return 1.37 + } 1.38 + // BUG(paddy): We really ought to break client verification out into its own helper functions, but I think it may depend on which grant_type is used... 1.39 + redirectURI := r.PostFormValue("redirect_uri") 1.40 + clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth() 1.41 + if !fromAuthHeader { 1.42 + clientIDStr = r.PostFormValue("client_id") 1.43 + } 1.44 + clientID, err := uuid.Parse(clientIDStr) 1.45 + if err != nil { 1.46 + w.WriteHeader(http.StatusUnauthorized) 1.47 + if fromAuthHeader { 1.48 + w.Header().Set("WWW-Authenticate", "Basic") 1.49 + } 1.50 + renderJSONError(enc, "invalid_client") 1.51 + return 1.52 + } 1.53 + client, err := context.GetClient(clientID) 1.54 + if err != nil { 1.55 + if err == ErrClientNotFound { 1.56 + w.WriteHeader(http.StatusUnauthorized) 1.57 + renderJSONError(enc, "invalid_client") 1.58 + } else { 1.59 + w.WriteHeader(http.StatusInternalServerError) 1.60 + renderJSONError(enc, "server_error") 1.61 + } 1.62 + return 1.63 + } 1.64 + if client.Secret != clientSecret { 1.65 + w.WriteHeader(http.StatusUnauthorized) 1.66 + if fromAuthHeader { 1.67 + w.Header().Set("WWW-Authenticate", "Basic") 1.68 + } 1.69 + renderJSONError(enc, "invalid_client") 1.70 + return 1.71 + } 1.72 + grant, err := context.GetGrant(code) 1.73 + if err != nil { 1.74 + if err == ErrGrantNotFound { 1.75 + w.WriteHeader(http.StatusBadRequest) 1.76 + renderJSONError(enc, "invalid_grant") 1.77 + return 1.78 + } 1.79 + w.WriteHeader(http.StatusInternalServerError) 1.80 + renderJSONError(enc, "server_error") 1.81 + return 1.82 + } 1.83 + if grant.RedirectURI != redirectURI { 1.84 + w.WriteHeader(http.StatusBadRequest) 1.85 + renderJSONError(enc, "invalid_grant") 1.86 + return 1.87 + } 1.88 + if !grant.ClientID.Equal(clientID) { 1.89 + w.WriteHeader(http.StatusBadRequest) 1.90 + renderJSONError(enc, "invalid_grant") 1.91 + return 1.92 + } 1.93 + return grant.Scope, grant.ProfileID, true 1.94 +}
2.1 --- a/oauth2.go Sat Dec 06 02:03:20 2014 -0500 2.2 +++ b/oauth2.go Sat Dec 06 03:33:11 2014 -0500 2.3 @@ -9,6 +9,7 @@ 2.4 "log" 2.5 "net/http" 2.6 "net/url" 2.7 + "sync" 2.8 "time" 2.9 2.10 "code.secondbit.org/pass" 2.11 @@ -34,8 +35,35 @@ 2.12 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme") 2.13 // ErrNoSession is returned when no session ID is passed with a request. 2.14 ErrNoSession = errors.New("no session ID found") 2.15 + 2.16 + grantTypesMap = grantTypes{types: map[string]GrantType{}} 2.17 ) 2.18 2.19 +type grantTypes struct { 2.20 + types map[string]GrantType 2.21 + sync.RWMutex 2.22 +} 2.23 + 2.24 +// GrantType defines a set of functions and metadata around a specific authorization grant strategy. 2.25 +// 2.26 +// The Validate function will be called when requests are made that match the GrantType, and should write any 2.27 +// errors to the ResponseWriter. It is responsible for determining if the grant is valid and a token should be issued. 2.28 +// It must return the scope the grant was for and the ID of the Profile that issued the grant, as well as if the grant 2.29 +// is valid or not. It must not be nil. 2.30 +// 2.31 +// The Invalidate function will be called when the grant has successfully generated a token and the token has successfully 2.32 +// been conveyed to the user. The Invalidate function is always called asynchronously, outside the request. It should take 2.33 +// care of marking the grant as used, if the GrantType requires grants to be one-time only grants. The Invalidate function 2.34 +// can be nil. 2.35 +// 2.36 +// IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client 2.37 +// will be issued a refresh token. 2.38 +type GrantType struct { 2.39 + Validate func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) 2.40 + Invalidate func(r *http.Request, context Context) bool 2.41 + IssuesRefresh bool 2.42 +} 2.43 + 2.44 type tokenResponse struct { 2.45 AccessToken string `json:"access_token"` 2.46 TokenType string `json:"token_type,omitempty"` 2.47 @@ -49,6 +77,27 @@ 2.48 URI string `json:"error_uri,omitempty"` 2.49 } 2.50 2.51 +// RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining 2.52 +// an access token, the associated GrantType's properties will be used. 2.53 +// 2.54 +// RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic 2.55 +// if a GrantType tries to register under a string that already has a GrantType registered for it. 2.56 +func RegisterGrantType(name string, g GrantType) { 2.57 + grantTypesMap.Lock() 2.58 + defer grantTypesMap.Unlock() 2.59 + if _, ok := grantTypesMap.types[name]; ok { 2.60 + panic("Duplicate registration of grant_type " + name) 2.61 + } 2.62 + grantTypesMap.types[name] = g 2.63 +} 2.64 + 2.65 +func findGrantType(name string) (GrantType, bool) { 2.66 + grantTypesMap.RLock() 2.67 + defer grantTypesMap.RUnlock() 2.68 + t, ok := grantTypesMap.types[name] 2.69 + return t, ok 2.70 +} 2.71 + 2.72 func renderJSONError(enc *json.Encoder, errorType string) { 2.73 err := enc.Encode(errorResponse{ 2.74 Error: errorType, 2.75 @@ -299,84 +348,32 @@ 2.76 // GetTokenHandler allows a client to exchange an authorization grant for an 2.77 // access token. See RFC 6749 Section 4.1.3. 2.78 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) { 2.79 - // BUG(paddy): this function is an absolute mess. Honestly, it should be more general purpose, with each grant mode being called based on the grant_type POST form value. Basically, each grant type could have its own function, accepting the Request and ResponseWriter, and returning a boolean if the request should continue being processed or not. The function is in charge of validating the grant, which offers more flexible extensibiliy when adding grant types and easier testing, while also making the token distribution code easier to reuse in an elegant way. There is a minor problem that the token distribution code has some dependencies on the grant type being used (some grant types don't issue refresh tokens, for example) but that's a minor issue. Something like a map of string -> custom grantType struct would fix that. The struct could hold the function to call to validate the grant type and booleans that impact the token issuance. Then you do a map lookup based on the POST form value, and call the function or read the booleans as needed. If we use the same "register" pattern found in database/sql drivers, allowing grant types to register themselves, it'll be possible to add a grant type without even touching this function. 2.80 enc := json.NewEncoder(w) 2.81 grantType := r.PostFormValue("grant_type") 2.82 - if grantType != "authorization_code" { 2.83 + gt, ok := findGrantType(grantType) 2.84 + if !ok { 2.85 w.WriteHeader(http.StatusBadRequest) 2.86 renderJSONError(enc, "invalid_request") 2.87 return 2.88 } 2.89 - code := r.PostFormValue("code") 2.90 - if code == "" { 2.91 - w.WriteHeader(http.StatusBadRequest) 2.92 - renderJSONError(enc, "invalid_request") 2.93 + scope, profileID, valid := gt.Validate(w, r, context) 2.94 + if !valid { 2.95 return 2.96 } 2.97 - redirectURI := r.PostFormValue("redirect_uri") 2.98 - clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth() 2.99 - if !fromAuthHeader { 2.100 - clientIDStr = r.PostFormValue("client_id") 2.101 - } 2.102 - clientID, err := uuid.Parse(clientIDStr) 2.103 - if err != nil { 2.104 - w.WriteHeader(http.StatusUnauthorized) 2.105 - if fromAuthHeader { 2.106 - w.Header().Set("WWW-Authenticate", "Basic") 2.107 - } 2.108 - renderJSONError(enc, "invalid_client") 2.109 - return 2.110 - } 2.111 - client, err := context.GetClient(clientID) 2.112 - if err != nil { 2.113 - if err == ErrClientNotFound { 2.114 - w.WriteHeader(http.StatusUnauthorized) 2.115 - renderJSONError(enc, "invalid_client") 2.116 - } else { 2.117 - w.WriteHeader(http.StatusInternalServerError) 2.118 - renderJSONError(enc, "server_error") 2.119 - } 2.120 - return 2.121 - } 2.122 - if client.Secret != clientSecret { 2.123 - w.WriteHeader(http.StatusUnauthorized) 2.124 - if fromAuthHeader { 2.125 - w.Header().Set("WWW-Authenticate", "Basic") 2.126 - } 2.127 - renderJSONError(enc, "invalid_client") 2.128 - return 2.129 - } 2.130 - grant, err := context.GetGrant(code) 2.131 - if err != nil { 2.132 - if err == ErrGrantNotFound { 2.133 - w.WriteHeader(http.StatusBadRequest) 2.134 - renderJSONError(enc, "invalid_grant") 2.135 - return 2.136 - } 2.137 - w.WriteHeader(http.StatusInternalServerError) 2.138 - renderJSONError(enc, "server_error") 2.139 - return 2.140 - } 2.141 - if grant.RedirectURI != redirectURI { 2.142 - w.WriteHeader(http.StatusBadRequest) 2.143 - renderJSONError(enc, "invalid_grant") 2.144 - return 2.145 - } 2.146 - if !grant.ClientID.Equal(clientID) { 2.147 - w.WriteHeader(http.StatusBadRequest) 2.148 - renderJSONError(enc, "invalid_grant") 2.149 - return 2.150 + refresh := "" 2.151 + if gt.IssuesRefresh { 2.152 + refresh = uuid.NewID().String() 2.153 } 2.154 token := Token{ 2.155 AccessToken: uuid.NewID().String(), 2.156 - RefreshToken: uuid.NewID().String(), 2.157 + RefreshToken: refresh, 2.158 Created: time.Now(), 2.159 ExpiresIn: defaultTokenExpiration, 2.160 TokenType: "bearer", 2.161 - Scope: grant.Scope, 2.162 - ProfileID: grant.ProfileID, 2.163 + Scope: scope, 2.164 + ProfileID: profileID, 2.165 } 2.166 - err = context.SaveToken(token) 2.167 + err := context.SaveToken(token) 2.168 if err != nil { 2.169 w.WriteHeader(http.StatusInternalServerError) 2.170 renderJSONError(enc, "server_error")