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