auth

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

14:fe7f358ecbe6 Go to Latest

auth/access.go

Return NilClientError when updating with nil client. When updateClient is called with a nil client, reaturn a NilClientError.

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 }