auth

Paddy 2014-08-16 Parent:9fe684b33b3d

20:0ccace901036 Go to Latest

auth/access.go

Check session before rendering confirmation page. The confirmation page should not be rendered until the session is set. Check the request method, then check the session, then finally render the confirmation page, should we need to.

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