auth

Paddy 2015-06-29 Parent:581c60f8dd23 Child:b7e685839a1b

175:aa14e29b666f Go to Latest

auth/oauth2.go

Create Docker image for authd. Create a Dockerfile for authd, which will wrap the compiled Go binary up into a tiny little Docker image. Create an authd/build-docker.sh script that will build the statically-linked binary in a Docker container, so the authd Docker image can use it. We had to include ca-certificates.crt in the Dockerfile, as well, so we could communicate over SSL with things. A wrapper.sh file is included that will pull the JWT_SECRET environment variable out of a kubernetes secrets file, which is a handy wrapper to have. Finally, we added the authd/docker-authd binary to the .hgignore.

History
paddy@51 1 package auth
paddy@51 2
paddy@51 3 import (
paddy@147 4 "crypto/rand"
paddy@147 5 "encoding/hex"
paddy@69 6 "encoding/json"
paddy@69 7 "errors"
paddy@61 8 "html/template"
paddy@147 9 "io"
paddy@77 10 "log"
paddy@51 11 "net/http"
paddy@60 12 "net/url"
paddy@122 13 "strconv"
paddy@135 14 "strings"
paddy@84 15 "sync"
paddy@60 16 "time"
paddy@56 17
paddy@107 18 "code.secondbit.org/uuid.hg"
paddy@82 19 "github.com/gorilla/mux"
paddy@51 20 )
paddy@51 21
paddy@60 22 const (
paddy@87 23 defaultAuthorizationCodeExpiration = 600 // default to ten minute grant expirations
paddy@87 24 getAuthorizationCodeTemplateName = "get_grant"
paddy@60 25 )
paddy@51 26
paddy@69 27 var (
paddy@69 28 // ErrNoAuth is returned when an Authorization header is not present or is empty.
paddy@69 29 ErrNoAuth = errors.New("no authorization header supplied")
paddy@69 30 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
paddy@69 31 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
paddy@69 32 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
paddy@69 33 ErrIncorrectAuth = errors.New("invalid authentication")
paddy@69 34 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
paddy@69 35 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
paddy@69 36 // ErrNoSession is returned when no session ID is passed with a request.
paddy@69 37 ErrNoSession = errors.New("no session ID found")
paddy@84 38
paddy@84 39 grantTypesMap = grantTypes{types: map[string]GrantType{}}
paddy@69 40 )
paddy@69 41
paddy@84 42 type grantTypes struct {
paddy@84 43 types map[string]GrantType
paddy@84 44 sync.RWMutex
paddy@84 45 }
paddy@84 46
paddy@84 47 // GrantType defines a set of functions and metadata around a specific authorization grant strategy.
paddy@84 48 //
paddy@84 49 // The Validate function will be called when requests are made that match the GrantType, and should write any
paddy@84 50 // errors to the ResponseWriter. It is responsible for determining if the grant is valid and a token should be issued.
paddy@84 51 // 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
paddy@84 52 // is valid or not. It must not be nil.
paddy@84 53 //
paddy@84 54 // The Invalidate function will be called when the grant has successfully generated a token and the token has successfully
paddy@84 55 // been conveyed to the user. The Invalidate function is always called asynchronously, outside the request. It should take
paddy@84 56 // care of marking the grant as used, if the GrantType requires grants to be one-time only grants. The Invalidate function
paddy@84 57 // can be nil.
paddy@84 58 //
paddy@84 59 // IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client
paddy@84 60 // will be issued a refresh token.
paddy@85 61 //
paddy@123 62 // AllowsPublic determines whether the GrantType should allow public clients to use that grant. If true, clients without
paddy@123 63 // credentials will be able to use the grant to obtain a token.
paddy@123 64 //
paddy@124 65 // AuditString should return the string that will be saved in the resulting Token's CreatedFrom field, as an audit log of how
paddy@124 66 // the Token was authorized.
paddy@124 67 //
paddy@85 68 // The ReturnToken will be called when a token is created and needs to be returned to the client. If it returns true, the token
paddy@85 69 // was successfully returned and the Invalidate function will be called asynchronously.
paddy@84 70 type GrantType struct {
paddy@163 71 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool)
paddy@90 72 Invalidate func(r *http.Request, context Context) error
paddy@85 73 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
paddy@124 74 AuditString func(r *http.Request) string
paddy@84 75 IssuesRefresh bool
paddy@123 76 AllowsPublic bool
paddy@84 77 }
paddy@84 78
paddy@69 79 type tokenResponse struct {
paddy@69 80 AccessToken string `json:"access_token"`
paddy@69 81 TokenType string `json:"token_type,omitempty"`
paddy@69 82 ExpiresIn int32 `json:"expires_in,omitempty"`
paddy@69 83 RefreshToken string `json:"refresh_token,omitempty"`
paddy@69 84 }
paddy@69 85
paddy@82 86 type errorResponse struct {
paddy@82 87 Error string `json:"error"`
paddy@82 88 Description string `json:"error_description,omitempty"`
paddy@82 89 URI string `json:"error_uri,omitempty"`
paddy@82 90 }
paddy@82 91
paddy@84 92 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
paddy@84 93 // an access token, the associated GrantType's properties will be used.
paddy@84 94 //
paddy@84 95 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
paddy@84 96 // if a GrantType tries to register under a string that already has a GrantType registered for it.
paddy@84 97 func RegisterGrantType(name string, g GrantType) {
paddy@84 98 grantTypesMap.Lock()
paddy@84 99 defer grantTypesMap.Unlock()
paddy@84 100 if _, ok := grantTypesMap.types[name]; ok {
paddy@84 101 panic("Duplicate registration of grant_type " + name)
paddy@84 102 }
paddy@84 103 grantTypesMap.types[name] = g
paddy@84 104 }
paddy@84 105
paddy@84 106 func findGrantType(name string) (GrantType, bool) {
paddy@84 107 grantTypesMap.RLock()
paddy@84 108 defer grantTypesMap.RUnlock()
paddy@84 109 t, ok := grantTypesMap.types[name]
paddy@84 110 return t, ok
paddy@84 111 }
paddy@84 112
paddy@82 113 func renderJSONError(enc *json.Encoder, errorType string) {
paddy@82 114 err := enc.Encode(errorResponse{
paddy@82 115 Error: errorType,
paddy@82 116 })
paddy@82 117 if err != nil {
paddy@90 118 log.Println(err)
paddy@69 119 }
paddy@69 120 }
paddy@69 121
paddy@86 122 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
paddy@86 123 // according to the spec. See RFC 6479, Section 4.1.4.
paddy@85 124 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
paddy@85 125 enc := json.NewEncoder(w)
paddy@85 126 resp := tokenResponse{
paddy@85 127 AccessToken: token.AccessToken,
paddy@85 128 RefreshToken: token.RefreshToken,
paddy@85 129 ExpiresIn: token.ExpiresIn,
paddy@85 130 TokenType: token.TokenType,
paddy@85 131 }
paddy@109 132 w.Header().Set("Content-Type", "application/json")
paddy@85 133 err := enc.Encode(resp)
paddy@85 134 if err != nil {
paddy@90 135 log.Println(err)
paddy@85 136 return false
paddy@85 137 }
paddy@85 138 return true
paddy@85 139 }
paddy@85 140
paddy@77 141 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
paddy@77 142 func RegisterOAuth2(r *mux.Router, context Context) {
paddy@87 143 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
paddy@77 144 r.Handle("/token", wrap(context, GetTokenHandler))
paddy@77 145 }
paddy@77 146
paddy@87 147 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
paddy@57 148 // to their data. See RFC 6749, Section 4.1.
paddy@87 149 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 150 session, err := checkCookie(r, context)
paddy@69 151 if err != nil {
paddy@76 152 if err == ErrNoSession || err == ErrInvalidSession {
paddy@77 153 redir := buildLoginRedirect(r, context)
paddy@77 154 if redir == "" {
paddy@77 155 log.Println("No login URL configured.")
paddy@77 156 w.WriteHeader(http.StatusInternalServerError)
paddy@87 157 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@77 158 "internal_error": template.HTML("Missing login URL."),
paddy@77 159 })
paddy@77 160 return
paddy@77 161 }
paddy@77 162 http.Redirect(w, r, redir, http.StatusFound)
paddy@77 163 return
paddy@69 164 }
paddy@77 165 log.Println(err.Error())
paddy@77 166 w.WriteHeader(http.StatusInternalServerError)
paddy@87 167 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@77 168 "internal_error": template.HTML(err.Error()),
paddy@77 169 })
paddy@77 170 return
paddy@69 171 }
paddy@56 172 if r.URL.Query().Get("client_id") == "" {
paddy@56 173 w.WriteHeader(http.StatusBadRequest)
paddy@87 174 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 175 "error": template.HTML("Client ID must be specified in the request."),
paddy@56 176 })
paddy@56 177 return
paddy@56 178 }
paddy@56 179 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
paddy@56 180 if err != nil {
paddy@56 181 w.WriteHeader(http.StatusBadRequest)
paddy@87 182 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 183 "error": template.HTML("client_id is not a valid Client ID."),
paddy@56 184 })
paddy@56 185 return
paddy@56 186 }
paddy@64 187 redirectURI := r.URL.Query().Get("redirect_uri")
paddy@56 188 client, err := context.GetClient(clientID)
paddy@56 189 if err != nil {
paddy@59 190 if err == ErrClientNotFound {
paddy@59 191 w.WriteHeader(http.StatusBadRequest)
paddy@87 192 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 193 "error": template.HTML("The specified Client couldn’t be found."),
paddy@59 194 })
paddy@59 195 } else {
paddy@77 196 log.Println(err.Error())
paddy@59 197 w.WriteHeader(http.StatusInternalServerError)
paddy@87 198 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 199 "internal_error": template.HTML(err.Error()),
paddy@59 200 })
paddy@59 201 }
paddy@56 202 return
paddy@56 203 }
paddy@128 204 // BUG(paddy): Checking if the redirect URI is valid should be a helper function.
paddy@128 205
paddy@56 206 // whether a redirect URI is valid or not depends on the number of endpoints
paddy@56 207 // the client has registered
paddy@56 208 numEndpoints, err := context.CountEndpoints(clientID)
paddy@56 209 if err != nil {
paddy@77 210 log.Println(err.Error())
paddy@56 211 w.WriteHeader(http.StatusInternalServerError)
paddy@87 212 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 213 "internal_error": template.HTML(err.Error()),
paddy@56 214 })
paddy@56 215 return
paddy@56 216 }
paddy@56 217 var validURI bool
paddy@58 218 if redirectURI != "" {
paddy@58 219 validURI, err = context.CheckEndpoint(clientID, redirectURI)
paddy@56 220 if err != nil {
paddy@116 221 if err == ErrEndpointURINotURL {
paddy@116 222 w.WriteHeader(http.StatusBadRequest)
paddy@116 223 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@116 224 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@116 225 })
paddy@116 226 return
paddy@116 227 }
paddy@77 228 log.Println(err.Error())
paddy@56 229 w.WriteHeader(http.StatusInternalServerError)
paddy@87 230 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 231 "internal_error": template.HTML(err.Error()),
paddy@56 232 })
paddy@56 233 return
paddy@56 234 }
paddy@56 235 } else if redirectURI == "" && numEndpoints == 1 {
paddy@56 236 // if we don't specify the endpoint and there's only one endpoint, the
paddy@56 237 // request is valid, and we're redirecting to that one endpoint
paddy@56 238 validURI = true
paddy@56 239 endpoints, err := context.ListEndpoints(clientID, 1, 0)
paddy@56 240 if err != nil {
paddy@77 241 log.Println(err.Error())
paddy@56 242 w.WriteHeader(http.StatusInternalServerError)
paddy@87 243 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 244 "internal_error": template.HTML(err.Error()),
paddy@56 245 })
paddy@56 246 return
paddy@56 247 }
paddy@56 248 if len(endpoints) != 1 {
paddy@56 249 validURI = false
paddy@56 250 } else {
paddy@116 251 redirectURI = endpoints[0].URI
paddy@56 252 }
paddy@56 253 } else {
paddy@56 254 validURI = false
paddy@56 255 }
paddy@56 256 if !validURI {
paddy@56 257 w.WriteHeader(http.StatusBadRequest)
paddy@87 258 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 259 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@56 260 })
paddy@56 261 return
paddy@56 262 }
paddy@116 263 redirectURL, err := url.Parse(redirectURI)
paddy@116 264 if err != nil {
paddy@116 265 w.WriteHeader(http.StatusBadRequest)
paddy@116 266 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@116 267 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@116 268 })
paddy@116 269 return
paddy@116 270 }
paddy@60 271 state := r.URL.Query().Get("state")
paddy@122 272 responseType := r.URL.Query().Get("response_type")
paddy@122 273 q := redirectURL.Query()
paddy@122 274 q.Add("state", state)
paddy@122 275 if responseType != "code" && responseType != "token" {
paddy@65 276 q.Add("error", "invalid_request")
paddy@65 277 redirectURL.RawQuery = q.Encode()
paddy@60 278 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 279 return
paddy@56 280 }
paddy@135 281 scopeParams := strings.Split(r.URL.Query().Get("scope"), " ")
paddy@135 282 scopes, err := context.GetScopes(scopeParams)
paddy@135 283 if err != nil {
paddy@153 284 if err == ErrScopeNotFound {
paddy@135 285 q.Add("error", "invalid_scope")
paddy@135 286 redirectURL.RawQuery = q.Encode()
paddy@135 287 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@135 288 return
paddy@135 289 }
paddy@135 290 log.Println("Error retrieving scopes:", err)
paddy@135 291 q.Add("error", "server_error")
paddy@135 292 redirectURL.RawQuery = q.Encode()
paddy@135 293 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@135 294 return
paddy@135 295 }
paddy@56 296 if r.Method == "POST" {
paddy@132 297 if checkCSRF(r, session) != nil {
paddy@132 298 log.Println("CSRF attempt detected.")
paddy@132 299 w.WriteHeader(http.StatusInternalServerError)
paddy@132 300 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@132 301 "error": template.HTML("There was an error authenticating your request."),
paddy@132 302 })
paddy@132 303 return
paddy@132 304 }
paddy@56 305 if r.PostFormValue("grant") == "approved" {
paddy@122 306 var fragment bool
paddy@122 307 switch responseType {
paddy@122 308 case "code":
paddy@147 309 code := make([]byte, 16)
paddy@147 310 _, err := io.ReadFull(rand.Reader, code)
paddy@147 311 if err != nil {
paddy@147 312 log.Printf("Error generating code: %#+v\n", err)
paddy@147 313 q.Add("error", "server_error")
paddy@147 314 break
paddy@147 315 }
paddy@122 316 authCode := AuthorizationCode{
paddy@147 317 Code: hex.EncodeToString(code),
paddy@122 318 Created: time.Now(),
paddy@122 319 ExpiresIn: defaultAuthorizationCodeExpiration,
paddy@122 320 ClientID: clientID,
paddy@163 321 Scopes: scopes,
paddy@122 322 RedirectURI: r.URL.Query().Get("redirect_uri"),
paddy@122 323 State: state,
paddy@122 324 ProfileID: session.ProfileID,
paddy@122 325 }
paddy@147 326 err = context.SaveAuthorizationCode(authCode)
paddy@122 327 if err != nil {
paddy@122 328 log.Println("Error saving authorization code:", err)
paddy@122 329 q.Add("error", "server_error")
paddy@122 330 break
paddy@122 331 }
paddy@147 332 q.Add("code", authCode.Code)
paddy@122 333 case "token":
paddy@122 334 token := Token{
paddy@122 335 Created: time.Now(),
paddy@125 336 CreatedFrom: "implicit",
paddy@122 337 ExpiresIn: defaultTokenExpiration,
paddy@122 338 TokenType: "bearer",
paddy@163 339 Scopes: scopes,
paddy@122 340 ProfileID: session.ProfileID,
paddy@125 341 ClientID: clientID,
paddy@122 342 }
paddy@168 343 access, err := token.GenerateAccessToken(context.config.JWTPrivateKey)
paddy@168 344 if err != nil {
paddy@168 345 log.Printf("Error signing token: %+v\n", err)
paddy@168 346 q.Add("error", "server_error")
paddy@168 347 break
paddy@168 348 }
paddy@168 349 token.AccessToken = access
paddy@168 350 err = context.SaveToken(token)
paddy@122 351 if err != nil {
paddy@122 352 log.Println("Error saving token:", err)
paddy@122 353 q.Add("error", "server_error")
paddy@122 354 break
paddy@122 355 }
paddy@122 356 q = url.Values{} // we're not altering the querystring, so don't clone it
paddy@122 357 q.Add("access_token", token.AccessToken)
paddy@122 358 q.Add("token_type", token.TokenType)
paddy@122 359 q.Add("expires_in", strconv.FormatInt(int64(token.ExpiresIn), 10))
paddy@163 360 q.Add("scope", strings.Join(token.Scopes.Strings(), " "))
paddy@122 361 q.Add("state", state) // we wiped out the old values, so we need to set the state again
paddy@122 362 fragment = true
paddy@60 363 }
paddy@122 364 if fragment {
paddy@122 365 redirectURL.Fragment = q.Encode()
paddy@122 366 } else {
paddy@66 367 redirectURL.RawQuery = q.Encode()
paddy@60 368 }
paddy@60 369 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 370 return
paddy@56 371 }
paddy@66 372 q.Add("error", "access_denied")
paddy@66 373 redirectURL.RawQuery = q.Encode()
paddy@60 374 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 375 return
paddy@56 376 }
paddy@85 377 profile, err := context.GetProfileByID(session.ProfileID)
paddy@85 378 if err != nil {
paddy@122 379 log.Println("Error getting profile from session:", err)
paddy@85 380 q.Add("error", "server_error")
paddy@85 381 redirectURL.RawQuery = q.Encode()
paddy@85 382 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@85 383 return
paddy@85 384 }
paddy@51 385 w.WriteHeader(http.StatusOK)
paddy@87 386 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@85 387 "client": client,
paddy@85 388 "redirectURL": redirectURL,
paddy@135 389 "scopes": scopes,
paddy@85 390 "profile": profile,
paddy@132 391 "csrftoken": session.CSRFToken,
paddy@56 392 })
paddy@51 393 }
paddy@68 394
paddy@69 395 // GetTokenHandler allows a client to exchange an authorization grant for an
paddy@69 396 // access token. See RFC 6749 Section 4.1.3.
paddy@69 397 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 398 enc := json.NewEncoder(w)
paddy@69 399 grantType := r.PostFormValue("grant_type")
paddy@84 400 gt, ok := findGrantType(grantType)
paddy@84 401 if !ok {
paddy@82 402 w.WriteHeader(http.StatusBadRequest)
paddy@82 403 renderJSONError(enc, "invalid_request")
paddy@69 404 return
paddy@69 405 }
paddy@123 406 clientID, success := verifyClient(w, r, gt.AllowsPublic, context)
paddy@123 407 if !success {
paddy@123 408 return
paddy@123 409 }
paddy@135 410 scopes, profileID, valid := gt.Validate(w, r, context)
paddy@84 411 if !valid {
paddy@69 412 return
paddy@69 413 }
paddy@84 414 refresh := ""
paddy@84 415 if gt.IssuesRefresh {
paddy@84 416 refresh = uuid.NewID().String()
paddy@69 417 }
paddy@69 418 token := Token{
paddy@125 419 AccessToken: uuid.NewID().String(),
paddy@125 420 RefreshToken: refresh,
paddy@125 421 Created: time.Now(),
paddy@125 422 CreatedFrom: gt.AuditString(r),
paddy@125 423 ExpiresIn: defaultTokenExpiration,
paddy@125 424 TokenType: "bearer",
paddy@135 425 Scopes: scopes,
paddy@125 426 ProfileID: profileID,
paddy@125 427 ClientID: clientID,
paddy@69 428 }
paddy@168 429 access, err := token.GenerateAccessToken(context.config.JWTPrivateKey)
paddy@168 430 if err != nil {
paddy@168 431 log.Printf("Error signing token: %+v\n", err)
paddy@168 432 w.WriteHeader(http.StatusInternalServerError)
paddy@168 433 renderJSONError(enc, "server_error")
paddy@168 434 return
paddy@168 435 }
paddy@168 436 token.AccessToken = access
paddy@168 437 err = context.SaveToken(token)
paddy@69 438 if err != nil {
paddy@82 439 w.WriteHeader(http.StatusInternalServerError)
paddy@82 440 renderJSONError(enc, "server_error")
paddy@81 441 return
paddy@69 442 }
paddy@85 443 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
paddy@85 444 go gt.Invalidate(r, context)
paddy@69 445 }
paddy@69 446 }