auth
auth/access.go
Handle more errors. Handle client errors. Match redirect handling with the spec.
| paddy@0 | 1 package oauth2 |
| 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@5 | 93 } else { |
| paddy@5 | 94 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@5 | 95 } |
| 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@0 | 102 // TODO: return error |
| paddy@0 | 103 return |
| paddy@0 | 104 } |
| paddy@5 | 105 if authData.RedirectURI == "" { |
| paddy@5 | 106 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid redirect on grant.", ctx.Config.DocumentationDomain) |
| paddy@0 | 107 return |
| paddy@5 | 108 } |
| paddy@1 | 109 if authData.IsExpired() { |
| paddy@3 | 110 ctx.RenderJSONError(w, ErrorInvalidGrant, "Authorization is expired.", ctx.Config.DocumentationDomain) |
| paddy@3 | 111 return |
| paddy@0 | 112 } |
| paddy@0 | 113 |
| paddy@0 | 114 // code must be from the client |
| paddy@1 | 115 if !authData.Client.ID.Equal(client.ID) { |
| paddy@3 | 116 ctx.RenderJSONError(w, ErrorInvalidGrant, "Grant issued to another client.", ctx.Config.DocumentationDomain) |
| paddy@0 | 117 return |
| paddy@0 | 118 } |
| paddy@0 | 119 |
| paddy@0 | 120 // check redirect uri |
| paddy@1 | 121 redirectURI := r.Form.Get("redirect_uri") |
| paddy@1 | 122 if redirectURI == "" { |
| paddy@1 | 123 redirectURI = client.RedirectURI |
| paddy@0 | 124 } |
| paddy@1 | 125 if err = validateURI(client.RedirectURI, redirectURI); err != nil { |
| paddy@3 | 126 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match client.", ctx.Config.DocumentationDomain) |
| paddy@0 | 127 return |
| paddy@0 | 128 } |
| paddy@1 | 129 if authData.RedirectURI != redirectURI { |
| paddy@3 | 130 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match auth redirect.", ctx.Config.DocumentationDomain) |
| paddy@0 | 131 return |
| paddy@0 | 132 } |
| paddy@0 | 133 |
| paddy@1 | 134 data := AccessData{ |
| paddy@1 | 135 AuthRequest: AuthRequest{ |
| paddy@1 | 136 Client: client, |
| paddy@1 | 137 RedirectURI: redirectURI, |
| paddy@1 | 138 Scope: authData.Scope, |
| paddy@1 | 139 }, |
| paddy@1 | 140 PreviousAuthorizeData: &authData, |
| paddy@1 | 141 } |
| paddy@1 | 142 |
| paddy@1 | 143 err = fillTokens(&data, true, ctx) |
| paddy@1 | 144 if err != nil { |
| paddy@1 | 145 // TODO: return error |
| paddy@1 | 146 return |
| paddy@1 | 147 } |
| paddy@4 | 148 ctx.RenderJSONToken(w, data) |
| paddy@0 | 149 } |
| paddy@0 | 150 |
| paddy@0 | 151 func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 152 // get client authentication |
| paddy@0 | 153 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@3 | 154 |
| paddy@0 | 155 if err != nil { |
| paddy@3 | 156 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@0 | 157 return |
| paddy@0 | 158 } |
| paddy@0 | 159 |
| paddy@1 | 160 code := r.Form.Get("refresh_token") |
| paddy@0 | 161 |
| paddy@0 | 162 // "refresh_token" is required |
| paddy@1 | 163 if code == "" { |
| paddy@3 | 164 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing refresh token.", ctx.Config.DocumentationDomain) |
| paddy@0 | 165 return |
| paddy@0 | 166 } |
| paddy@0 | 167 |
| paddy@0 | 168 // must have a valid client |
| paddy@1 | 169 client, err := getClient(auth, ctx) |
| paddy@0 | 170 if err != nil { |
| paddy@5 | 171 if err == ClientNotFoundError || err == InvalidClientError { |
| paddy@5 | 172 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@5 | 173 } else { |
| paddy@5 | 174 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@5 | 175 } |
| paddy@0 | 176 return |
| paddy@0 | 177 } |
| paddy@0 | 178 |
| paddy@0 | 179 // must be a valid refresh code |
| paddy@1 | 180 refreshData, err := ctx.Tokens.GetRefresh(code) |
| paddy@0 | 181 if err != nil { |
| paddy@0 | 182 // TODO: return error |
| paddy@0 | 183 return |
| paddy@0 | 184 } |
| paddy@0 | 185 |
| paddy@0 | 186 // client must be the same as the previous token |
| paddy@1 | 187 if !refreshData.Client.ID.Equal(client.ID) { |
| paddy@3 | 188 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token issued to another client.", ctx.Config.DocumentationDomain) |
| paddy@0 | 189 return |
| paddy@0 | 190 } |
| paddy@0 | 191 |
| paddy@1 | 192 scope := r.Form.Get("scope") |
| paddy@1 | 193 if scope == "" { |
| paddy@1 | 194 scope = refreshData.Scope |
| paddy@0 | 195 } |
| paddy@0 | 196 |
| paddy@1 | 197 data := AccessData{ |
| paddy@1 | 198 AuthRequest: AuthRequest{ |
| paddy@5 | 199 Client: client, |
| paddy@5 | 200 Scope: scope, |
| paddy@1 | 201 }, |
| paddy@1 | 202 PreviousAccessData: &refreshData, |
| paddy@1 | 203 } |
| paddy@1 | 204 err = fillTokens(&data, true, ctx) |
| paddy@1 | 205 if err != nil { |
| paddy@1 | 206 // TODO: return error |
| paddy@1 | 207 return |
| paddy@1 | 208 } |
| paddy@4 | 209 ctx.RenderJSONToken(w, data) |
| paddy@0 | 210 } |
| paddy@0 | 211 |
| paddy@0 | 212 func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 213 // get client authentication |
| paddy@0 | 214 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 215 if err != nil { |
| paddy@4 | 216 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@0 | 217 return |
| paddy@0 | 218 } |
| paddy@0 | 219 |
| paddy@1 | 220 username := r.Form.Get("username") |
| paddy@1 | 221 password := r.Form.Get("password") |
| paddy@1 | 222 scope := r.Form.Get("scope") |
| paddy@0 | 223 |
| paddy@0 | 224 // "username" and "password" is required |
| paddy@1 | 225 if username == "" || password == "" { |
| paddy@4 | 226 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing credentials.", ctx.Config.DocumentationDomain) |
| paddy@0 | 227 return |
| paddy@0 | 228 } |
| paddy@0 | 229 |
| paddy@0 | 230 // must have a valid client |
| paddy@1 | 231 client, err := getClient(auth, ctx) |
| paddy@0 | 232 if err != nil { |
| paddy@5 | 233 if err == ClientNotFoundError || err == InvalidClientError { |
| paddy@5 | 234 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@5 | 235 } else { |
| paddy@5 | 236 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@5 | 237 } |
| paddy@0 | 238 return |
| paddy@0 | 239 } |
| paddy@0 | 240 |
| paddy@1 | 241 _, err = ctx.Profiles.GetProfile(username, password) |
| paddy@1 | 242 if err != nil { |
| paddy@1 | 243 // TODO: return error |
| paddy@1 | 244 return |
| paddy@1 | 245 } |
| paddy@1 | 246 |
| paddy@1 | 247 data := AccessData{ |
| paddy@1 | 248 AuthRequest: AuthRequest{ |
| paddy@5 | 249 Client: client, |
| paddy@5 | 250 Scope: scope, |
| paddy@1 | 251 }, |
| paddy@1 | 252 } |
| paddy@1 | 253 |
| paddy@1 | 254 err = fillTokens(&data, true, ctx) |
| paddy@1 | 255 if err != nil { |
| paddy@1 | 256 // TODO: return error |
| paddy@1 | 257 return |
| paddy@1 | 258 } |
| paddy@4 | 259 ctx.RenderJSONToken(w, data) |
| paddy@0 | 260 } |
| paddy@0 | 261 |
| paddy@0 | 262 func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 263 // get client authentication |
| paddy@0 | 264 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 265 if err != nil { |
| paddy@4 | 266 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@0 | 267 return |
| paddy@0 | 268 } |
| paddy@0 | 269 |
| paddy@1 | 270 scope := r.Form.Get("scope") |
| paddy@0 | 271 |
| paddy@0 | 272 // must have a valid client |
| paddy@1 | 273 client, err := getClient(auth, ctx) |
| paddy@0 | 274 if err != nil { |
| paddy@5 | 275 if err == ClientNotFoundError || err == InvalidClientError { |
| paddy@5 | 276 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@5 | 277 } else { |
| paddy@5 | 278 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@5 | 279 } |
| paddy@0 | 280 return |
| paddy@0 | 281 } |
| paddy@0 | 282 |
| paddy@1 | 283 data := AccessData{ |
| paddy@1 | 284 AuthRequest: AuthRequest{ |
| paddy@5 | 285 Client: client, |
| paddy@5 | 286 Scope: scope, |
| paddy@1 | 287 }, |
| paddy@1 | 288 } |
| paddy@0 | 289 |
| paddy@1 | 290 err = fillTokens(&data, true, ctx) |
| paddy@0 | 291 if err != nil { |
| paddy@0 | 292 // TODO: return error |
| paddy@0 | 293 return |
| paddy@0 | 294 } |
| paddy@4 | 295 ctx.RenderJSONToken(w, data) |
| paddy@0 | 296 } |
| paddy@0 | 297 |
| paddy@1 | 298 func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error { |
| paddy@0 | 299 var err error |
| paddy@0 | 300 |
| paddy@0 | 301 // generate access token |
| paddy@1 | 302 data.AccessToken = newToken() |
| paddy@1 | 303 if includeRefresh { |
| paddy@1 | 304 data.RefreshToken = newToken() |
| paddy@0 | 305 } |
| paddy@0 | 306 |
| paddy@0 | 307 // save access token |
| paddy@1 | 308 err = ctx.Tokens.SaveAccess(*data) |
| paddy@0 | 309 if err != nil { |
| paddy@1 | 310 // TODO: abstract out error |
| paddy@1 | 311 return err |
| paddy@0 | 312 } |
| paddy@0 | 313 |
| paddy@0 | 314 // remove authorization token |
| paddy@1 | 315 if data.PreviousAuthorizeData != nil { |
| paddy@1 | 316 err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code) |
| paddy@0 | 317 if err != nil { |
| paddy@0 | 318 // TODO: log error |
| paddy@0 | 319 } |
| paddy@0 | 320 } |
| paddy@0 | 321 |
| paddy@0 | 322 // remove previous access token |
| paddy@1 | 323 if data.PreviousAccessData != nil { |
| paddy@1 | 324 if data.PreviousAccessData.RefreshToken != "" { |
| paddy@1 | 325 err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken) |
| paddy@0 | 326 if err != nil { |
| paddy@0 | 327 // TODO: log error |
| paddy@0 | 328 } |
| paddy@0 | 329 } |
| paddy@1 | 330 err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken) |
| paddy@0 | 331 if err != nil { |
| paddy@0 | 332 // TODO: log error |
| paddy@0 | 333 } |
| paddy@0 | 334 } |
| paddy@0 | 335 |
| paddy@1 | 336 data.TokenType = ctx.Config.TokenType |
| paddy@1 | 337 data.ExpiresIn = ctx.Config.AccessExpiration |
| paddy@1 | 338 data.CreatedAt = time.Now() |
| paddy@1 | 339 return nil |
| paddy@0 | 340 } |
| paddy@0 | 341 |
| paddy@1 | 342 func (data AccessData) GetRedirect(fragment bool) (string, error) { |
| paddy@1 | 343 u, err := url.Parse(data.RedirectURI) |
| paddy@1 | 344 if err != nil { |
| paddy@1 | 345 return "", err |
| paddy@1 | 346 } |
| paddy@1 | 347 |
| paddy@1 | 348 // add parameters |
| paddy@1 | 349 q := u.Query() |
| paddy@1 | 350 q.Set("access_token", data.AccessToken) |
| paddy@1 | 351 q.Set("token_type", data.TokenType) |
| paddy@1 | 352 q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10)) |
| paddy@1 | 353 if data.RefreshToken != "" { |
| paddy@1 | 354 q.Set("refresh_token", data.RefreshToken) |
| paddy@1 | 355 } |
| paddy@1 | 356 if data.Scope != "" { |
| paddy@1 | 357 q.Set("scope", data.Scope) |
| paddy@1 | 358 } |
| paddy@1 | 359 if len(data.ProfileID) > 0 { |
| paddy@1 | 360 q.Set("profile", data.ProfileID.String()) |
| paddy@1 | 361 } |
| paddy@1 | 362 if fragment { |
| paddy@1 | 363 u.RawQuery = "" |
| paddy@1 | 364 u.Fragment = q.Encode() |
| paddy@1 | 365 } else { |
| paddy@1 | 366 u.RawQuery = q.Encode() |
| paddy@1 | 367 } |
| paddy@1 | 368 |
| paddy@1 | 369 return u.String(), nil |
| paddy@1 | 370 } |
| paddy@0 | 371 |
| paddy@0 | 372 // getClient looks up and authenticates the basic auth using the given |
| paddy@0 | 373 // storage. Sets an error on the response if auth fails or a server error occurs. |
| paddy@0 | 374 func getClient(auth BasicAuth, ctx Context) (Client, error) { |
| paddy@0 | 375 id, err := uuid.Parse(auth.Username) |
| paddy@0 | 376 if err != nil { |
| paddy@0 | 377 return Client{}, err |
| paddy@0 | 378 } |
| paddy@1 | 379 client, err := ctx.Clients.GetClient(id) |
| paddy@0 | 380 if err != nil { |
| paddy@5 | 381 if err == ClientNotFoundError { |
| paddy@5 | 382 return Client{}, err |
| paddy@5 | 383 } |
| paddy@5 | 384 // TODO: log error |
| paddy@5 | 385 return Client{}, InternalServerError |
| paddy@0 | 386 } |
| paddy@0 | 387 if client.Secret != auth.Password { |
| paddy@5 | 388 return Client{}, InvalidClientError |
| paddy@0 | 389 } |
| paddy@0 | 390 if client.RedirectURI == "" { |
| paddy@5 | 391 return Client{}, InvalidClientError |
| paddy@0 | 392 } |
| paddy@0 | 393 return client, nil |
| paddy@0 | 394 } |