auth

Paddy 2014-08-13 Parent:244ac84003b3 Child:fc5df8e68c7b

12:63e86d129238 Go to Latest

auth/access.go

Consistently handle context in client storage interface. Let's at least be consistent about passing or not passing context to the client storage interface. Most our methods don't take the context, so let's just remove it.

History
paddy@6 1 package auth
paddy@0 2
paddy@0 3 import (
paddy@0 4 "net/http"
paddy@1 5 "net/url"
paddy@0 6 "time"
paddy@0 7
paddy@1 8 "strconv"
paddy@0 9 "secondbit.org/uuid"
paddy@0 10 )
paddy@0 11
paddy@0 12 // GrantType is the type for OAuth param `grant_type`
paddy@0 13 type GrantType string
paddy@0 14
paddy@0 15 const (
paddy@0 16 AuthorizationCodeGrant GrantType = "authorization_code"
paddy@0 17 RefreshTokenGrant = "refresh_token"
paddy@0 18 PasswordGrant = "password"
paddy@0 19 ClientCredentialsGrant = "client_credentials"
paddy@0 20 )
paddy@0 21
paddy@0 22 // AccessData represents an access grant (tokens, expiration, client, etc)
paddy@0 23 type AccessData struct {
paddy@1 24 PreviousAuthorizeData *AuthorizeData
paddy@1 25 PreviousAccessData *AccessData // previous access data, when refreshing
paddy@1 26 AccessToken string
paddy@1 27 RefreshToken string
paddy@1 28 ExpiresIn int32
paddy@1 29 CreatedAt time.Time
paddy@1 30 TokenType string
paddy@1 31 ProfileID uuid.ID
paddy@1 32 AuthRequest
paddy@0 33 }
paddy@0 34
paddy@0 35 // IsExpired returns true if access expired
paddy@0 36 func (d *AccessData) IsExpired() bool {
paddy@0 37 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second).Before(time.Now())
paddy@0 38 }
paddy@0 39
paddy@0 40 // ExpireAt returns the expiration date
paddy@0 41 func (d *AccessData) ExpireAt() time.Time {
paddy@0 42 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second)
paddy@0 43 }
paddy@0 44
paddy@0 45 // HandleOAuth2AccessRequest is the http.HandlerFunc for handling access token requests.
paddy@0 46 func HandleOAuth2AccessRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
paddy@0 47 // Only allow GET or POST
paddy@0 48 if r.Method != "POST" {
paddy@1 49 if r.Method != "GET" || !ctx.Config.AllowGetAccessRequest {
paddy@3 50 ctx.RenderJSONError(w, ErrorInvalidRequest, "Invalid request method.", ctx.Config.DocumentationDomain)
paddy@0 51 return
paddy@0 52 }
paddy@0 53 }
paddy@0 54
paddy@0 55 grantType := GrantType(r.Form.Get("grant_type"))
paddy@0 56 if ctx.Config.AllowedAccessTypes.Exists(grantType) {
paddy@0 57 switch grantType {
paddy@0 58 case AuthorizationCodeGrant:
paddy@0 59 handleAuthorizationCodeRequest(w, r, ctx)
paddy@0 60 case RefreshTokenGrant:
paddy@0 61 handleRefreshTokenRequest(w, r, ctx)
paddy@0 62 case PasswordGrant:
paddy@0 63 handlePasswordRequest(w, r, ctx)
paddy@0 64 case ClientCredentialsGrant:
paddy@0 65 handleClientCredentialsRequest(w, r, ctx)
paddy@0 66 default:
paddy@3 67 ctx.RenderJSONError(w, ErrorUnsupportedGrantType, "Unsupported grant type.", ctx.Config.DocumentationDomain)
paddy@0 68 return
paddy@0 69 }
paddy@0 70 }
paddy@0 71 }
paddy@0 72
paddy@0 73 func handleAuthorizationCodeRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
paddy@0 74 // get client authentication
paddy@0 75 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
paddy@0 76 if err != nil {
paddy@3 77 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
paddy@0 78 return
paddy@0 79 }
paddy@0 80
paddy@1 81 code := r.Form.Get("code")
paddy@0 82 // "code" is required
paddy@1 83 if code == "" {
paddy@3 84 ctx.RenderJSONError(w, ErrorInvalidRequest, "Code must be supplied.", ctx.Config.DocumentationDomain)
paddy@0 85 return
paddy@0 86 }
paddy@0 87
paddy@0 88 // must have a valid client
paddy@1 89 client, err := getClient(auth, ctx)
paddy@0 90 if err != nil {
paddy@5 91 if err == ClientNotFoundError || err == InvalidClientError {
paddy@5 92 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
paddy@9 93 return
paddy@5 94 }
paddy@9 95 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
paddy@0 96 return
paddy@0 97 }
paddy@0 98
paddy@0 99 // must be a valid authorization code
paddy@1 100 authData, err := ctx.Tokens.GetAuthorization(code)
paddy@0 101 if err != nil {
paddy@9 102 if err == AuthorizationNotFoundError {
paddy@9 103 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid authorization.", ctx.Config.DocumentationDomain)
paddy@9 104 return
paddy@9 105 }
paddy@9 106 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
paddy@0 107 return
paddy@0 108 }
paddy@5 109 if authData.RedirectURI == "" {
paddy@5 110 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid redirect on grant.", ctx.Config.DocumentationDomain)
paddy@0 111 return
paddy@5 112 }
paddy@1 113 if authData.IsExpired() {
paddy@3 114 ctx.RenderJSONError(w, ErrorInvalidGrant, "Authorization is expired.", ctx.Config.DocumentationDomain)
paddy@3 115 return
paddy@0 116 }
paddy@0 117
paddy@0 118 // code must be from the client
paddy@1 119 if !authData.Client.ID.Equal(client.ID) {
paddy@3 120 ctx.RenderJSONError(w, ErrorInvalidGrant, "Grant issued to another client.", ctx.Config.DocumentationDomain)
paddy@0 121 return
paddy@0 122 }
paddy@0 123
paddy@0 124 // check redirect uri
paddy@1 125 redirectURI := r.Form.Get("redirect_uri")
paddy@1 126 if redirectURI == "" {
paddy@1 127 redirectURI = client.RedirectURI
paddy@0 128 }
paddy@1 129 if err = validateURI(client.RedirectURI, redirectURI); err != nil {
paddy@3 130 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match client.", ctx.Config.DocumentationDomain)
paddy@0 131 return
paddy@0 132 }
paddy@1 133 if authData.RedirectURI != redirectURI {
paddy@3 134 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match auth redirect.", ctx.Config.DocumentationDomain)
paddy@0 135 return
paddy@0 136 }
paddy@0 137
paddy@1 138 data := AccessData{
paddy@1 139 AuthRequest: AuthRequest{
paddy@1 140 Client: client,
paddy@1 141 RedirectURI: redirectURI,
paddy@1 142 Scope: authData.Scope,
paddy@1 143 },
paddy@1 144 PreviousAuthorizeData: &authData,
paddy@1 145 }
paddy@1 146
paddy@1 147 err = fillTokens(&data, true, ctx)
paddy@1 148 if err != nil {
paddy@7 149 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
paddy@1 150 return
paddy@1 151 }
paddy@4 152 ctx.RenderJSONToken(w, data)
paddy@0 153 }
paddy@0 154
paddy@0 155 func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
paddy@0 156 // get client authentication
paddy@0 157 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
paddy@3 158
paddy@0 159 if err != nil {
paddy@3 160 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
paddy@0 161 return
paddy@0 162 }
paddy@0 163
paddy@1 164 code := r.Form.Get("refresh_token")
paddy@0 165
paddy@0 166 // "refresh_token" is required
paddy@1 167 if code == "" {
paddy@3 168 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing refresh token.", ctx.Config.DocumentationDomain)
paddy@0 169 return
paddy@0 170 }
paddy@0 171
paddy@0 172 // must have a valid client
paddy@1 173 client, err := getClient(auth, ctx)
paddy@0 174 if err != nil {
paddy@5 175 if err == ClientNotFoundError || err == InvalidClientError {
paddy@5 176 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
paddy@9 177 return
paddy@5 178 }
paddy@9 179 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
paddy@0 180 return
paddy@0 181 }
paddy@0 182
paddy@0 183 // must be a valid refresh code
paddy@1 184 refreshData, err := ctx.Tokens.GetRefresh(code)
paddy@0 185 if err != nil {
paddy@9 186 if err == TokenNotFoundError {
paddy@9 187 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token not valid.", ctx.Config.DocumentationDomain)
paddy@9 188 return
paddy@9 189 }
paddy@9 190 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
paddy@0 191 return
paddy@0 192 }
paddy@0 193
paddy@0 194 // client must be the same as the previous token
paddy@1 195 if !refreshData.Client.ID.Equal(client.ID) {
paddy@3 196 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token issued to another client.", ctx.Config.DocumentationDomain)
paddy@0 197 return
paddy@0 198 }
paddy@0 199
paddy@1 200 scope := r.Form.Get("scope")
paddy@1 201 if scope == "" {
paddy@1 202 scope = refreshData.Scope
paddy@0 203 }
paddy@0 204
paddy@1 205 data := AccessData{
paddy@1 206 AuthRequest: AuthRequest{
paddy@5 207 Client: client,
paddy@5 208 Scope: scope,
paddy@1 209 },
paddy@1 210 PreviousAccessData: &refreshData,
paddy@1 211 }
paddy@1 212 err = fillTokens(&data, true, ctx)
paddy@1 213 if err != nil {
paddy@7 214 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
paddy@1 215 return
paddy@1 216 }
paddy@4 217 ctx.RenderJSONToken(w, data)
paddy@0 218 }
paddy@0 219
paddy@0 220 func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
paddy@0 221 // get client authentication
paddy@0 222 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
paddy@0 223 if err != nil {
paddy@4 224 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
paddy@0 225 return
paddy@0 226 }
paddy@0 227
paddy@1 228 username := r.Form.Get("username")
paddy@1 229 password := r.Form.Get("password")
paddy@1 230 scope := r.Form.Get("scope")
paddy@0 231
paddy@0 232 // "username" and "password" is required
paddy@1 233 if username == "" || password == "" {
paddy@4 234 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing credentials.", ctx.Config.DocumentationDomain)
paddy@0 235 return
paddy@0 236 }
paddy@0 237
paddy@0 238 // must have a valid client
paddy@1 239 client, err := getClient(auth, ctx)
paddy@0 240 if err != nil {
paddy@5 241 if err == ClientNotFoundError || err == InvalidClientError {
paddy@5 242 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
paddy@9 243 return
paddy@5 244 }
paddy@9 245 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
paddy@0 246 return
paddy@0 247 }
paddy@0 248
paddy@1 249 _, err = ctx.Profiles.GetProfile(username, password)
paddy@1 250 if err != nil {
paddy@9 251 if err == ProfileNotFoundError {
paddy@9 252 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid credentials.", ctx.Config.DocumentationDomain)
paddy@9 253 return
paddy@9 254 }
paddy@9 255 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
paddy@1 256 return
paddy@1 257 }
paddy@1 258
paddy@1 259 data := AccessData{
paddy@1 260 AuthRequest: AuthRequest{
paddy@5 261 Client: client,
paddy@5 262 Scope: scope,
paddy@1 263 },
paddy@1 264 }
paddy@1 265
paddy@1 266 err = fillTokens(&data, true, ctx)
paddy@1 267 if err != nil {
paddy@7 268 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
paddy@1 269 return
paddy@1 270 }
paddy@4 271 ctx.RenderJSONToken(w, data)
paddy@0 272 }
paddy@0 273
paddy@0 274 func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
paddy@0 275 // get client authentication
paddy@0 276 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
paddy@0 277 if err != nil {
paddy@4 278 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
paddy@0 279 return
paddy@0 280 }
paddy@0 281
paddy@1 282 scope := r.Form.Get("scope")
paddy@0 283
paddy@0 284 // must have a valid client
paddy@1 285 client, err := getClient(auth, ctx)
paddy@0 286 if err != nil {
paddy@5 287 if err == ClientNotFoundError || err == InvalidClientError {
paddy@5 288 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
paddy@9 289 return
paddy@5 290 }
paddy@9 291 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
paddy@0 292 return
paddy@0 293 }
paddy@0 294
paddy@1 295 data := AccessData{
paddy@1 296 AuthRequest: AuthRequest{
paddy@5 297 Client: client,
paddy@5 298 Scope: scope,
paddy@1 299 },
paddy@1 300 }
paddy@0 301
paddy@1 302 err = fillTokens(&data, true, ctx)
paddy@0 303 if err != nil {
paddy@7 304 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
paddy@0 305 return
paddy@0 306 }
paddy@4 307 ctx.RenderJSONToken(w, data)
paddy@0 308 }
paddy@0 309
paddy@1 310 func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error {
paddy@0 311 var err error
paddy@0 312
paddy@0 313 // generate access token
paddy@1 314 data.AccessToken = newToken()
paddy@1 315 if includeRefresh {
paddy@1 316 data.RefreshToken = newToken()
paddy@0 317 }
paddy@0 318
paddy@0 319 // save access token
paddy@1 320 err = ctx.Tokens.SaveAccess(*data)
paddy@0 321 if err != nil {
paddy@10 322 if ctx.Log != nil {
paddy@10 323 ctx.Log.Printf("Error writing access token: %s\n", err)
paddy@10 324 }
paddy@7 325 return InternalServerError
paddy@0 326 }
paddy@0 327
paddy@0 328 // remove authorization token
paddy@1 329 if data.PreviousAuthorizeData != nil {
paddy@1 330 err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code)
paddy@10 331 if err != nil && ctx.Log != nil {
paddy@10 332 ctx.Log.Printf("Error removing previous auth data (%s): %s\n", data.PreviousAuthorizeData.Code, err)
paddy@0 333 }
paddy@0 334 }
paddy@0 335
paddy@0 336 // remove previous access token
paddy@1 337 if data.PreviousAccessData != nil {
paddy@1 338 if data.PreviousAccessData.RefreshToken != "" {
paddy@1 339 err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken)
paddy@10 340 if err != nil && ctx.Log != nil {
paddy@10 341 ctx.Log.Printf("Error removing previous refresh token (%s): %s\n", data.PreviousAccessData.RefreshToken, err)
paddy@0 342 }
paddy@0 343 }
paddy@1 344 err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken)
paddy@10 345 if err != nil && ctx.Log != nil {
paddy@10 346 ctx.Log.Printf("Error removing previous access token (%s): %s\n", data.PreviousAccessData.AccessToken, err)
paddy@0 347 }
paddy@0 348 }
paddy@0 349
paddy@1 350 data.TokenType = ctx.Config.TokenType
paddy@1 351 data.ExpiresIn = ctx.Config.AccessExpiration
paddy@1 352 data.CreatedAt = time.Now()
paddy@1 353 return nil
paddy@0 354 }
paddy@0 355
paddy@1 356 func (data AccessData) GetRedirect(fragment bool) (string, error) {
paddy@1 357 u, err := url.Parse(data.RedirectURI)
paddy@1 358 if err != nil {
paddy@1 359 return "", err
paddy@1 360 }
paddy@1 361
paddy@1 362 // add parameters
paddy@1 363 q := u.Query()
paddy@1 364 q.Set("access_token", data.AccessToken)
paddy@1 365 q.Set("token_type", data.TokenType)
paddy@1 366 q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10))
paddy@1 367 if data.RefreshToken != "" {
paddy@1 368 q.Set("refresh_token", data.RefreshToken)
paddy@1 369 }
paddy@1 370 if data.Scope != "" {
paddy@1 371 q.Set("scope", data.Scope)
paddy@1 372 }
paddy@1 373 if len(data.ProfileID) > 0 {
paddy@1 374 q.Set("profile", data.ProfileID.String())
paddy@1 375 }
paddy@1 376 if fragment {
paddy@1 377 u.RawQuery = ""
paddy@1 378 u.Fragment = q.Encode()
paddy@1 379 } else {
paddy@1 380 u.RawQuery = q.Encode()
paddy@1 381 }
paddy@1 382
paddy@1 383 return u.String(), nil
paddy@1 384 }
paddy@0 385
paddy@0 386 // getClient looks up and authenticates the basic auth using the given
paddy@0 387 // storage. Sets an error on the response if auth fails or a server error occurs.
paddy@0 388 func getClient(auth BasicAuth, ctx Context) (Client, error) {
paddy@0 389 id, err := uuid.Parse(auth.Username)
paddy@0 390 if err != nil {
paddy@0 391 return Client{}, err
paddy@0 392 }
paddy@1 393 client, err := ctx.Clients.GetClient(id)
paddy@0 394 if err != nil {
paddy@5 395 if err == ClientNotFoundError {
paddy@5 396 return Client{}, err
paddy@5 397 }
paddy@10 398 if ctx.Log != nil {
paddy@10 399 ctx.Log.Printf("Error retrieving client %s: %s", id, err)
paddy@10 400 }
paddy@5 401 return Client{}, InternalServerError
paddy@0 402 }
paddy@0 403 if client.Secret != auth.Password {
paddy@5 404 return Client{}, InvalidClientError
paddy@0 405 }
paddy@0 406 if client.RedirectURI == "" {
paddy@5 407 return Client{}, InvalidClientError
paddy@0 408 }
paddy@0 409 return client, nil
paddy@0 410 }