auth

Paddy 2014-08-16 Parent:9fe684b33b3d

22:e6a44cfda658 Go to Latest

auth/access.go

Redirect unauthenticated users to the login page. Redirect unauthenticated users to the login page, and encode the current URL in the redirect_to param so that the user returns to the OAuth2 flow after they log in.

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 }