auth

Paddy 2014-12-06 Parent:11ad5eca2f82 Child:8630b108ce35

82:0a6e3f14b054 Go to Latest

auth/oauth2.go

Fix go vet, fix imports, render JSON errors, deprecate getBasicAuth. Fix logging functions in our test file that were causing go vet to report errors (and which would have obscured the test output). Move some imports around to make the imports more consistent and our pre-commit hook happy. Create a helper to render JSON errors, and actually render those errors when obtaining a token using a grant. Deprecate our custom getBasicAuth in favour of the new BasicAuth() method on net/http.*Request objects, which was introduced in Go 1.4 (meaning Go 1.4 is now a requirement for compiling this.)

History
paddy@51 1 package auth
paddy@51 2
paddy@51 3 import (
paddy@82 4 "crypto/sha256"
paddy@79 5 "encoding/hex"
paddy@69 6 "encoding/json"
paddy@69 7 "errors"
paddy@61 8 "html/template"
paddy@77 9 "log"
paddy@51 10 "net/http"
paddy@60 11 "net/url"
paddy@60 12 "time"
paddy@56 13
paddy@69 14 "code.secondbit.org/pass"
paddy@56 15 "code.secondbit.org/uuid"
paddy@82 16
paddy@82 17 "github.com/gorilla/mux"
paddy@51 18 )
paddy@51 19
paddy@60 20 const (
paddy@69 21 authCookieName = "auth"
paddy@69 22 defaultGrantExpiration = 600 // default to ten minute grant expirations
paddy@60 23 getGrantTemplateName = "get_grant"
paddy@60 24 )
paddy@51 25
paddy@69 26 var (
paddy@69 27 // ErrNoAuth is returned when an Authorization header is not present or is empty.
paddy@69 28 ErrNoAuth = errors.New("no authorization header supplied")
paddy@69 29 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
paddy@69 30 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
paddy@69 31 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
paddy@69 32 ErrIncorrectAuth = errors.New("invalid authentication")
paddy@69 33 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
paddy@69 34 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
paddy@69 35 // ErrNoSession is returned when no session ID is passed with a request.
paddy@69 36 ErrNoSession = errors.New("no session ID found")
paddy@69 37 )
paddy@69 38
paddy@69 39 type tokenResponse struct {
paddy@69 40 AccessToken string `json:"access_token"`
paddy@69 41 TokenType string `json:"token_type,omitempty"`
paddy@69 42 ExpiresIn int32 `json:"expires_in,omitempty"`
paddy@69 43 RefreshToken string `json:"refresh_token,omitempty"`
paddy@69 44 }
paddy@69 45
paddy@82 46 type errorResponse struct {
paddy@82 47 Error string `json:"error"`
paddy@82 48 Description string `json:"error_description,omitempty"`
paddy@82 49 URI string `json:"error_uri,omitempty"`
paddy@82 50 }
paddy@82 51
paddy@82 52 func renderJSONError(enc *json.Encoder, errorType string) {
paddy@82 53 err := enc.Encode(errorResponse{
paddy@82 54 Error: errorType,
paddy@82 55 })
paddy@82 56 if err != nil {
paddy@82 57 // TODO(paddy): log this or something
paddy@69 58 }
paddy@69 59 }
paddy@69 60
paddy@69 61 func checkCookie(r *http.Request, context Context) (Session, error) {
paddy@69 62 cookie, err := r.Cookie(authCookieName)
paddy@77 63 if err == http.ErrNoCookie {
paddy@77 64 return Session{}, ErrNoSession
paddy@77 65 } else if err != nil {
paddy@77 66 log.Println(err)
paddy@69 67 return Session{}, err
paddy@69 68 }
paddy@69 69 sess, err := context.GetSession(cookie.Value)
paddy@69 70 if err == ErrSessionNotFound {
paddy@69 71 return Session{}, ErrInvalidSession
paddy@69 72 } else if err != nil {
paddy@69 73 return Session{}, err
paddy@69 74 }
paddy@69 75 if !sess.Active {
paddy@69 76 return Session{}, ErrInvalidSession
paddy@69 77 }
paddy@69 78 return sess, nil
paddy@69 79 }
paddy@69 80
paddy@77 81 func buildLoginRedirect(r *http.Request, context Context) string {
paddy@77 82 if context.loginURI == nil {
paddy@77 83 return ""
paddy@77 84 }
paddy@77 85 uri := *context.loginURI
paddy@77 86 q := uri.Query()
paddy@78 87 q.Set("from", r.URL.String())
paddy@77 88 uri.RawQuery = q.Encode()
paddy@77 89 return uri.String()
paddy@77 90 }
paddy@77 91
paddy@69 92 func authenticate(user, passphrase string, context Context) (Profile, error) {
paddy@69 93 profile, err := context.GetProfileByLogin(user)
paddy@69 94 if err != nil {
paddy@79 95 if err == ErrProfileNotFound || err == ErrLoginNotFound {
paddy@69 96 return Profile{}, ErrIncorrectAuth
paddy@69 97 }
paddy@69 98 return Profile{}, err
paddy@69 99 }
paddy@69 100 switch profile.PassphraseScheme {
paddy@69 101 case 1:
paddy@79 102 realPass, err := hex.DecodeString(profile.Passphrase)
paddy@79 103 if err != nil {
paddy@79 104 return Profile{}, err
paddy@79 105 }
paddy@69 106 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
paddy@79 107 if !pass.Compare(candidate, realPass) {
paddy@69 108 return Profile{}, ErrIncorrectAuth
paddy@69 109 }
paddy@69 110 default:
paddy@69 111 return Profile{}, ErrInvalidPassphraseScheme
paddy@69 112 }
paddy@69 113 return profile, nil
paddy@69 114 }
paddy@69 115
paddy@77 116 func wrap(context Context, f func(w http.ResponseWriter, r *http.Request, context Context)) http.Handler {
paddy@77 117 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paddy@77 118 f(w, r, context)
paddy@77 119 })
paddy@77 120 }
paddy@77 121
paddy@77 122 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
paddy@77 123 func RegisterOAuth2(r *mux.Router, context Context) {
paddy@77 124 r.Handle("/authorize", wrap(context, GetGrantHandler))
paddy@77 125 r.Handle("/token", wrap(context, GetTokenHandler))
paddy@77 126 }
paddy@77 127
paddy@57 128 // GetGrantHandler presents and processes the page for asking a user to grant access
paddy@57 129 // to their data. See RFC 6749, Section 4.1.
paddy@51 130 func GetGrantHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 131 session, err := checkCookie(r, context)
paddy@69 132 if err != nil {
paddy@76 133 if err == ErrNoSession || err == ErrInvalidSession {
paddy@77 134 redir := buildLoginRedirect(r, context)
paddy@77 135 if redir == "" {
paddy@77 136 log.Println("No login URL configured.")
paddy@77 137 w.WriteHeader(http.StatusInternalServerError)
paddy@77 138 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@77 139 "internal_error": template.HTML("Missing login URL."),
paddy@77 140 })
paddy@77 141 return
paddy@77 142 }
paddy@77 143 http.Redirect(w, r, redir, http.StatusFound)
paddy@77 144 return
paddy@69 145 }
paddy@77 146 log.Println(err.Error())
paddy@77 147 w.WriteHeader(http.StatusInternalServerError)
paddy@77 148 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@77 149 "internal_error": template.HTML(err.Error()),
paddy@77 150 })
paddy@77 151 return
paddy@69 152 }
paddy@56 153 if r.URL.Query().Get("client_id") == "" {
paddy@56 154 w.WriteHeader(http.StatusBadRequest)
paddy@56 155 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 156 "error": template.HTML("Client ID must be specified in the request."),
paddy@56 157 })
paddy@56 158 return
paddy@56 159 }
paddy@56 160 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
paddy@56 161 if err != nil {
paddy@56 162 w.WriteHeader(http.StatusBadRequest)
paddy@56 163 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 164 "error": template.HTML("client_id is not a valid Client ID."),
paddy@56 165 })
paddy@56 166 return
paddy@56 167 }
paddy@64 168 redirectURI := r.URL.Query().Get("redirect_uri")
paddy@64 169 redirectURL, err := url.Parse(redirectURI)
paddy@64 170 if err != nil {
paddy@64 171 w.WriteHeader(http.StatusBadRequest)
paddy@64 172 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@64 173 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@64 174 })
paddy@64 175 return
paddy@64 176 }
paddy@56 177 client, err := context.GetClient(clientID)
paddy@56 178 if err != nil {
paddy@59 179 if err == ErrClientNotFound {
paddy@59 180 w.WriteHeader(http.StatusBadRequest)
paddy@59 181 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 182 "error": template.HTML("The specified Client couldn’t be found."),
paddy@59 183 })
paddy@59 184 } else {
paddy@77 185 log.Println(err.Error())
paddy@59 186 w.WriteHeader(http.StatusInternalServerError)
paddy@59 187 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 188 "internal_error": template.HTML(err.Error()),
paddy@59 189 })
paddy@59 190 }
paddy@56 191 return
paddy@56 192 }
paddy@56 193 // whether a redirect URI is valid or not depends on the number of endpoints
paddy@56 194 // the client has registered
paddy@56 195 numEndpoints, err := context.CountEndpoints(clientID)
paddy@56 196 if err != nil {
paddy@77 197 log.Println(err.Error())
paddy@56 198 w.WriteHeader(http.StatusInternalServerError)
paddy@56 199 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 200 "internal_error": template.HTML(err.Error()),
paddy@56 201 })
paddy@56 202 return
paddy@56 203 }
paddy@56 204 var validURI bool
paddy@58 205 if redirectURI != "" {
paddy@58 206 // BUG(paddy): We really should normalize URIs before trying to compare them.
paddy@58 207 validURI, err = context.CheckEndpoint(clientID, redirectURI)
paddy@56 208 if err != nil {
paddy@77 209 log.Println(err.Error())
paddy@56 210 w.WriteHeader(http.StatusInternalServerError)
paddy@56 211 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 212 "internal_error": template.HTML(err.Error()),
paddy@56 213 })
paddy@56 214 return
paddy@56 215 }
paddy@56 216 } else if redirectURI == "" && numEndpoints == 1 {
paddy@56 217 // if we don't specify the endpoint and there's only one endpoint, the
paddy@56 218 // request is valid, and we're redirecting to that one endpoint
paddy@56 219 validURI = true
paddy@56 220 endpoints, err := context.ListEndpoints(clientID, 1, 0)
paddy@56 221 if err != nil {
paddy@77 222 log.Println(err.Error())
paddy@56 223 w.WriteHeader(http.StatusInternalServerError)
paddy@56 224 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 225 "internal_error": template.HTML(err.Error()),
paddy@56 226 })
paddy@56 227 return
paddy@56 228 }
paddy@56 229 if len(endpoints) != 1 {
paddy@56 230 validURI = false
paddy@56 231 } else {
paddy@66 232 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
paddy@66 233 redirectURI = u.String()
paddy@66 234 redirectURL = &u
paddy@56 235 }
paddy@56 236 } else {
paddy@56 237 validURI = false
paddy@56 238 }
paddy@56 239 if !validURI {
paddy@56 240 w.WriteHeader(http.StatusBadRequest)
paddy@56 241 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 242 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@56 243 })
paddy@56 244 return
paddy@56 245 }
paddy@60 246 scope := r.URL.Query().Get("scope")
paddy@60 247 state := r.URL.Query().Get("state")
paddy@56 248 if r.URL.Query().Get("response_type") != "code" {
paddy@65 249 q := redirectURL.Query()
paddy@65 250 q.Add("error", "invalid_request")
paddy@65 251 q.Add("state", state)
paddy@65 252 redirectURL.RawQuery = q.Encode()
paddy@60 253 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 254 return
paddy@56 255 }
paddy@56 256 if r.Method == "POST" {
paddy@63 257 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
paddy@56 258 if r.PostFormValue("grant") == "approved" {
paddy@60 259 code := uuid.NewID().String()
paddy@60 260 grant := Grant{
paddy@60 261 Code: code,
paddy@60 262 Created: time.Now(),
paddy@60 263 ExpiresIn: defaultGrantExpiration,
paddy@60 264 ClientID: clientID,
paddy@60 265 Scope: scope,
paddy@69 266 RedirectURI: r.URL.Query().Get("redirect_uri"),
paddy@60 267 State: state,
paddy@69 268 ProfileID: session.ProfileID,
paddy@60 269 }
paddy@60 270 err := context.SaveGrant(grant)
paddy@60 271 if err != nil {
paddy@66 272 q := redirectURL.Query()
paddy@66 273 q.Add("error", "server_error")
paddy@66 274 q.Add("state", state)
paddy@66 275 redirectURL.RawQuery = q.Encode()
paddy@60 276 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 277 return
paddy@60 278 }
paddy@66 279 q := redirectURL.Query()
paddy@66 280 q.Add("code", code)
paddy@66 281 q.Add("state", state)
paddy@66 282 redirectURL.RawQuery = q.Encode()
paddy@60 283 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 284 return
paddy@56 285 }
paddy@66 286 q := redirectURL.Query()
paddy@66 287 q.Add("error", "access_denied")
paddy@66 288 q.Add("state", state)
paddy@66 289 redirectURL.RawQuery = q.Encode()
paddy@60 290 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 291 return
paddy@56 292 }
paddy@51 293 w.WriteHeader(http.StatusOK)
paddy@56 294 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@56 295 "client": client,
paddy@56 296 })
paddy@51 297 }
paddy@68 298
paddy@69 299 // GetTokenHandler allows a client to exchange an authorization grant for an
paddy@69 300 // access token. See RFC 6749 Section 4.1.3.
paddy@69 301 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 302 enc := json.NewEncoder(w)
paddy@69 303 grantType := r.PostFormValue("grant_type")
paddy@69 304 if grantType != "authorization_code" {
paddy@82 305 w.WriteHeader(http.StatusBadRequest)
paddy@82 306 renderJSONError(enc, "invalid_request")
paddy@69 307 return
paddy@69 308 }
paddy@69 309 code := r.PostFormValue("code")
paddy@69 310 if code == "" {
paddy@82 311 w.WriteHeader(http.StatusBadRequest)
paddy@82 312 renderJSONError(enc, "invalid_request")
paddy@69 313 return
paddy@69 314 }
paddy@69 315 redirectURI := r.PostFormValue("redirect_uri")
paddy@82 316 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
paddy@82 317 if !fromAuthHeader {
paddy@69 318 clientIDStr = r.PostFormValue("client_id")
paddy@69 319 }
paddy@69 320 clientID, err := uuid.Parse(clientIDStr)
paddy@69 321 if err != nil {
paddy@82 322 w.WriteHeader(http.StatusUnauthorized)
paddy@82 323 if fromAuthHeader {
paddy@82 324 w.Header().Set("WWW-Authenticate", "Basic")
paddy@82 325 }
paddy@82 326 renderJSONError(enc, "invalid_client")
paddy@69 327 return
paddy@69 328 }
paddy@69 329 client, err := context.GetClient(clientID)
paddy@69 330 if err != nil {
paddy@69 331 if err == ErrClientNotFound {
paddy@82 332 w.WriteHeader(http.StatusUnauthorized)
paddy@82 333 renderJSONError(enc, "invalid_client")
paddy@69 334 } else {
paddy@82 335 w.WriteHeader(http.StatusInternalServerError)
paddy@82 336 renderJSONError(enc, "server_error")
paddy@69 337 }
paddy@69 338 return
paddy@69 339 }
paddy@69 340 if client.Secret != clientSecret {
paddy@82 341 w.WriteHeader(http.StatusUnauthorized)
paddy@82 342 if fromAuthHeader {
paddy@82 343 w.Header().Set("WWW-Authenticate", "Basic")
paddy@82 344 }
paddy@82 345 renderJSONError(enc, "invalid_client")
paddy@69 346 return
paddy@69 347 }
paddy@69 348 grant, err := context.GetGrant(code)
paddy@69 349 if err != nil {
paddy@69 350 if err == ErrGrantNotFound {
paddy@82 351 w.WriteHeader(http.StatusBadRequest)
paddy@82 352 renderJSONError(enc, "invalid_grant")
paddy@69 353 return
paddy@69 354 }
paddy@82 355 w.WriteHeader(http.StatusInternalServerError)
paddy@82 356 renderJSONError(enc, "server_error")
paddy@81 357 return
paddy@69 358 }
paddy@69 359 if grant.RedirectURI != redirectURI {
paddy@82 360 w.WriteHeader(http.StatusBadRequest)
paddy@82 361 renderJSONError(enc, "invalid_grant")
paddy@81 362 return
paddy@69 363 }
paddy@69 364 if !grant.ClientID.Equal(clientID) {
paddy@82 365 w.WriteHeader(http.StatusBadRequest)
paddy@82 366 renderJSONError(enc, "invalid_grant")
paddy@81 367 return
paddy@69 368 }
paddy@69 369 token := Token{
paddy@69 370 AccessToken: uuid.NewID().String(),
paddy@69 371 RefreshToken: uuid.NewID().String(),
paddy@69 372 Created: time.Now(),
paddy@69 373 ExpiresIn: defaultTokenExpiration,
paddy@81 374 TokenType: "bearer",
paddy@69 375 Scope: grant.Scope,
paddy@69 376 ProfileID: grant.ProfileID,
paddy@69 377 }
paddy@69 378 err = context.SaveToken(token)
paddy@69 379 if err != nil {
paddy@82 380 w.WriteHeader(http.StatusInternalServerError)
paddy@82 381 renderJSONError(enc, "server_error")
paddy@81 382 return
paddy@69 383 }
paddy@69 384 resp := tokenResponse{
paddy@69 385 AccessToken: token.AccessToken,
paddy@69 386 RefreshToken: token.RefreshToken,
paddy@69 387 ExpiresIn: token.ExpiresIn,
paddy@69 388 TokenType: token.TokenType,
paddy@69 389 }
paddy@69 390 err = enc.Encode(resp)
paddy@69 391 if err != nil {
paddy@69 392 // TODO(paddy): log this or something
paddy@69 393 return
paddy@69 394 }
paddy@81 395 // BUG(paddy): we need to invalidate the grant for future requests
paddy@69 396 }
paddy@69 397
paddy@68 398 // TODO(paddy): exchange user credentials for access token
paddy@68 399 // TODO(paddy): exchange client credentials for access token
paddy@68 400 // TODO(paddy): implicit grant for access token
paddy@68 401 // TODO(paddy): exchange refresh token for access token