auth
2014-07-18
Child:7b9e0fc20256
auth/access.go
Start rewriting the repo. This code originally was a carbon copy of https://github.com/RangelReale/osin, but I am methodically stripping out the extensible nature of it for a simpler interface, while simultaneously bringing the style into line with the Ducky style.
| paddy@0 | 1 package oauth2 |
| paddy@0 | 2 |
| paddy@0 | 3 import ( |
| paddy@0 | 4 "net/http" |
| paddy@0 | 5 "time" |
| paddy@0 | 6 |
| paddy@0 | 7 "secondbit.org/uuid" |
| paddy@0 | 8 ) |
| paddy@0 | 9 |
| paddy@0 | 10 // GrantType is the type for OAuth param `grant_type` |
| paddy@0 | 11 type GrantType string |
| paddy@0 | 12 |
| paddy@0 | 13 const ( |
| paddy@0 | 14 AuthorizationCodeGrant GrantType = "authorization_code" |
| paddy@0 | 15 RefreshTokenGrant = "refresh_token" |
| paddy@0 | 16 PasswordGrant = "password" |
| paddy@0 | 17 ClientCredentialsGrant = "client_credentials" |
| paddy@0 | 18 AssertionGrant = "assertion" |
| paddy@0 | 19 ImplicitGrant = "__implicit" |
| paddy@0 | 20 ) |
| paddy@0 | 21 |
| paddy@0 | 22 // AccessRequest is a request for access tokens |
| paddy@0 | 23 type AccessRequest struct { |
| paddy@0 | 24 Code string |
| paddy@0 | 25 Client Client |
| paddy@0 | 26 AuthorizeData AuthorizeData |
| paddy@0 | 27 AccessData AccessData |
| paddy@0 | 28 RedirectURI string |
| paddy@0 | 29 Scope string |
| paddy@0 | 30 Username string |
| paddy@0 | 31 Password string |
| paddy@0 | 32 AssertionType string |
| paddy@0 | 33 Assertion string |
| paddy@0 | 34 |
| paddy@0 | 35 // Token expiration in seconds. Change if different from default |
| paddy@0 | 36 Expiration int32 |
| paddy@0 | 37 |
| paddy@0 | 38 // Set if a refresh token should be generated |
| paddy@0 | 39 GenerateRefresh bool |
| paddy@0 | 40 } |
| paddy@0 | 41 |
| paddy@0 | 42 // AccessData represents an access grant (tokens, expiration, client, etc) |
| paddy@0 | 43 type AccessData struct { |
| paddy@0 | 44 // Client information |
| paddy@0 | 45 Client Client |
| paddy@0 | 46 |
| paddy@0 | 47 // Authorize data, for authorization code |
| paddy@0 | 48 AuthorizeData *AuthorizeData |
| paddy@0 | 49 |
| paddy@0 | 50 // Previous access data, for refresh token |
| paddy@0 | 51 AccessData *AccessData |
| paddy@0 | 52 |
| paddy@0 | 53 // Access token |
| paddy@0 | 54 AccessToken string |
| paddy@0 | 55 |
| paddy@0 | 56 // Refresh Token. Can be blank |
| paddy@0 | 57 RefreshToken string |
| paddy@0 | 58 |
| paddy@0 | 59 // Token expiration in seconds |
| paddy@0 | 60 ExpiresIn int32 |
| paddy@0 | 61 |
| paddy@0 | 62 // Requested scope |
| paddy@0 | 63 Scope string |
| paddy@0 | 64 |
| paddy@0 | 65 // Redirect URI from request |
| paddy@0 | 66 RedirectURI string |
| paddy@0 | 67 |
| paddy@0 | 68 // Date created |
| paddy@0 | 69 CreatedAt time.Time |
| paddy@0 | 70 } |
| paddy@0 | 71 |
| paddy@0 | 72 // IsExpired returns true if access expired |
| paddy@0 | 73 func (d *AccessData) IsExpired() bool { |
| paddy@0 | 74 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second).Before(time.Now()) |
| paddy@0 | 75 } |
| paddy@0 | 76 |
| paddy@0 | 77 // ExpireAt returns the expiration date |
| paddy@0 | 78 func (d *AccessData) ExpireAt() time.Time { |
| paddy@0 | 79 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second) |
| paddy@0 | 80 } |
| paddy@0 | 81 |
| paddy@0 | 82 // AccessTokenGen generates access tokens |
| paddy@0 | 83 type AccessTokenGen interface { |
| paddy@0 | 84 GenerateAccessToken(data *AccessData, generaterefresh bool) (accesstoken string, refreshtoken string, err error) |
| paddy@0 | 85 } |
| paddy@0 | 86 |
| paddy@0 | 87 // HandleOAuth2AccessRequest is the http.HandlerFunc for handling access token requests. |
| paddy@0 | 88 func HandleOAuth2AccessRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 89 // Only allow GET or POST |
| paddy@0 | 90 if r.Method != "POST" { |
| paddy@0 | 91 if r.Method == "GET" && !ctx.Config.AllowGetAccessRequest { |
| paddy@0 | 92 // TODO: return error |
| paddy@0 | 93 return |
| paddy@0 | 94 } |
| paddy@0 | 95 } |
| paddy@0 | 96 |
| paddy@0 | 97 err := r.ParseForm() |
| paddy@0 | 98 if err != nil { |
| paddy@0 | 99 // TODO: return error |
| paddy@0 | 100 return |
| paddy@0 | 101 } |
| paddy@0 | 102 |
| paddy@0 | 103 grantType := GrantType(r.Form.Get("grant_type")) |
| paddy@0 | 104 if ctx.Config.AllowedAccessTypes.Exists(grantType) { |
| paddy@0 | 105 switch grantType { |
| paddy@0 | 106 case AuthorizationCodeGrant: |
| paddy@0 | 107 handleAuthorizationCodeRequest(w, r, ctx) |
| paddy@0 | 108 case RefreshTokenGrant: |
| paddy@0 | 109 handleRefreshTokenRequest(w, r, ctx) |
| paddy@0 | 110 case PasswordGrant: |
| paddy@0 | 111 handlePasswordRequest(w, r, ctx) |
| paddy@0 | 112 case ClientCredentialsGrant: |
| paddy@0 | 113 handleClientCredentialsRequest(w, r, ctx) |
| paddy@0 | 114 case AssertionGrant: |
| paddy@0 | 115 handleAssertionRequest(w, r, ctx) |
| paddy@0 | 116 default: |
| paddy@0 | 117 // TODO: return error |
| paddy@0 | 118 return |
| paddy@0 | 119 } |
| paddy@0 | 120 } |
| paddy@0 | 121 } |
| paddy@0 | 122 |
| paddy@0 | 123 func handleAuthorizationCodeRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 124 // get client authentication |
| paddy@0 | 125 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 126 if err != nil { |
| paddy@0 | 127 // TODO: return error |
| paddy@0 | 128 return |
| paddy@0 | 129 } |
| paddy@0 | 130 |
| paddy@0 | 131 // generate access token |
| paddy@0 | 132 ret := AccessRequest{ |
| paddy@0 | 133 Code: r.Form.Get("code"), |
| paddy@0 | 134 RedirectURI: r.Form.Get("redirect_uri"), |
| paddy@0 | 135 GenerateRefresh: true, |
| paddy@0 | 136 Expiration: ctx.Config.AccessExpiration, |
| paddy@0 | 137 } |
| paddy@0 | 138 |
| paddy@0 | 139 // "code" is required |
| paddy@0 | 140 if ret.Code == "" { |
| paddy@0 | 141 // TODO: return error |
| paddy@0 | 142 return |
| paddy@0 | 143 } |
| paddy@0 | 144 |
| paddy@0 | 145 // must have a valid client |
| paddy@0 | 146 ret.Client, err = getClient(auth, ctx) |
| paddy@0 | 147 if err != nil { |
| paddy@0 | 148 // TODO: return error |
| paddy@0 | 149 return |
| paddy@0 | 150 } |
| paddy@0 | 151 |
| paddy@0 | 152 // must be a valid authorization code |
| paddy@0 | 153 ret.AuthorizeData, err = loadAuthorize(ret.Code, ctx) |
| paddy@0 | 154 if err != nil { |
| paddy@0 | 155 // TODO: return error |
| paddy@0 | 156 return |
| paddy@0 | 157 } |
| paddy@0 | 158 if ret.AuthorizeData.Client.RedirectURI == "" { |
| paddy@0 | 159 // TODO: return error |
| paddy@0 | 160 return |
| paddy@0 | 161 } |
| paddy@0 | 162 if ret.AuthorizeData.IsExpired() { |
| paddy@0 | 163 return // TODO: return error |
| paddy@0 | 164 } |
| paddy@0 | 165 |
| paddy@0 | 166 // code must be from the client |
| paddy@0 | 167 if !ret.AuthorizeData.Client.ID.Equal(ret.Client.ID) { |
| paddy@0 | 168 // TODO: return error |
| paddy@0 | 169 return |
| paddy@0 | 170 } |
| paddy@0 | 171 |
| paddy@0 | 172 // check redirect uri |
| paddy@0 | 173 if ret.RedirectURI == "" { |
| paddy@0 | 174 ret.RedirectURI = ret.Client.RedirectURI |
| paddy@0 | 175 } |
| paddy@0 | 176 if err = ValidateURI(ret.Client.RedirectURI, ret.RedirectURI); err != nil { |
| paddy@0 | 177 // TODO: return error |
| paddy@0 | 178 return |
| paddy@0 | 179 } |
| paddy@0 | 180 if ret.AuthorizeData.RedirectURI != ret.RedirectURI { |
| paddy@0 | 181 // TODO: return error |
| paddy@0 | 182 return |
| paddy@0 | 183 } |
| paddy@0 | 184 |
| paddy@0 | 185 // set rest of data |
| paddy@0 | 186 ret.Scope = ret.AuthorizeData.Scope |
| paddy@0 | 187 // TODO: write ret |
| paddy@0 | 188 } |
| paddy@0 | 189 |
| paddy@0 | 190 func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 191 // get client authentication |
| paddy@0 | 192 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 193 if err != nil { |
| paddy@0 | 194 // TODO: return error |
| paddy@0 | 195 return |
| paddy@0 | 196 } |
| paddy@0 | 197 |
| paddy@0 | 198 // generate access token |
| paddy@0 | 199 ret := AccessRequest{ |
| paddy@0 | 200 Code: r.Form.Get("refresh_token"), |
| paddy@0 | 201 Scope: r.Form.Get("scope"), |
| paddy@0 | 202 GenerateRefresh: true, |
| paddy@0 | 203 Expiration: ctx.Config.AccessExpiration, |
| paddy@0 | 204 } |
| paddy@0 | 205 |
| paddy@0 | 206 // "refresh_token" is required |
| paddy@0 | 207 if ret.Code == "" { |
| paddy@0 | 208 // TODO: return error |
| paddy@0 | 209 return |
| paddy@0 | 210 } |
| paddy@0 | 211 |
| paddy@0 | 212 // must have a valid client |
| paddy@0 | 213 ret.Client, err = getClient(auth, ctx) |
| paddy@0 | 214 if err != nil { |
| paddy@0 | 215 // TODO: return error |
| paddy@0 | 216 return |
| paddy@0 | 217 } |
| paddy@0 | 218 |
| paddy@0 | 219 // must be a valid refresh code |
| paddy@0 | 220 ret.AccessData, err = loadRefresh(ret.Code, ctx) |
| paddy@0 | 221 if err != nil { |
| paddy@0 | 222 // TODO: return error |
| paddy@0 | 223 return |
| paddy@0 | 224 } |
| paddy@0 | 225 if ret.AccessData.Client.RedirectURI == "" { |
| paddy@0 | 226 // TODO: return error |
| paddy@0 | 227 return |
| paddy@0 | 228 } |
| paddy@0 | 229 |
| paddy@0 | 230 // client must be the same as the previous token |
| paddy@0 | 231 if !ret.AccessData.Client.ID.Equal(ret.Client.ID) { |
| paddy@0 | 232 // TODO: return error |
| paddy@0 | 233 return |
| paddy@0 | 234 |
| paddy@0 | 235 } |
| paddy@0 | 236 |
| paddy@0 | 237 // set rest of data |
| paddy@0 | 238 ret.RedirectURI = ret.AccessData.RedirectURI |
| paddy@0 | 239 if ret.Scope == "" { |
| paddy@0 | 240 ret.Scope = ret.AccessData.Scope |
| paddy@0 | 241 } |
| paddy@0 | 242 |
| paddy@0 | 243 // TODO: write ret |
| paddy@0 | 244 } |
| paddy@0 | 245 |
| paddy@0 | 246 func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 247 // get client authentication |
| paddy@0 | 248 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 249 if err != nil { |
| paddy@0 | 250 // TODO: return error |
| paddy@0 | 251 return |
| paddy@0 | 252 } |
| paddy@0 | 253 |
| paddy@0 | 254 // generate access token |
| paddy@0 | 255 ret := AccessRequest{ |
| paddy@0 | 256 Username: r.Form.Get("username"), |
| paddy@0 | 257 Password: r.Form.Get("password"), |
| paddy@0 | 258 Scope: r.Form.Get("scope"), |
| paddy@0 | 259 GenerateRefresh: true, |
| paddy@0 | 260 Expiration: ctx.Config.AccessExpiration, |
| paddy@0 | 261 } |
| paddy@0 | 262 |
| paddy@0 | 263 // "username" and "password" is required |
| paddy@0 | 264 if ret.Username == "" || ret.Password == "" { |
| paddy@0 | 265 // TODO: return error |
| paddy@0 | 266 return |
| paddy@0 | 267 } |
| paddy@0 | 268 |
| paddy@0 | 269 // must have a valid client |
| paddy@0 | 270 ret.Client, err = getClient(auth, ctx) |
| paddy@0 | 271 if err != nil { |
| paddy@0 | 272 // TODO: return error |
| paddy@0 | 273 return |
| paddy@0 | 274 } |
| paddy@0 | 275 |
| paddy@0 | 276 // set redirect uri |
| paddy@0 | 277 ret.RedirectURI = ret.Client.RedirectURI |
| paddy@0 | 278 |
| paddy@0 | 279 // TODO: write ret |
| paddy@0 | 280 } |
| paddy@0 | 281 |
| paddy@0 | 282 func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 283 // get client authentication |
| paddy@0 | 284 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 285 if err != nil { |
| paddy@0 | 286 // TODO: return error |
| paddy@0 | 287 return |
| paddy@0 | 288 } |
| paddy@0 | 289 |
| paddy@0 | 290 // generate access token |
| paddy@0 | 291 ret := AccessRequest{ |
| paddy@0 | 292 Scope: r.Form.Get("scope"), |
| paddy@0 | 293 GenerateRefresh: true, |
| paddy@0 | 294 Expiration: ctx.Config.AccessExpiration, |
| paddy@0 | 295 } |
| paddy@0 | 296 |
| paddy@0 | 297 // must have a valid client |
| paddy@0 | 298 ret.Client, err = getClient(auth, ctx) |
| paddy@0 | 299 if err != nil { |
| paddy@0 | 300 // TODO: return error |
| paddy@0 | 301 return |
| paddy@0 | 302 } |
| paddy@0 | 303 |
| paddy@0 | 304 // set redirect uri |
| paddy@0 | 305 ret.RedirectURI = ret.Client.RedirectURI |
| paddy@0 | 306 |
| paddy@0 | 307 // TODO: write ret |
| paddy@0 | 308 } |
| paddy@0 | 309 |
| paddy@0 | 310 func handleAssertionRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 311 // get client authentication |
| paddy@0 | 312 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 313 if err != nil { |
| paddy@0 | 314 // TODO: return error |
| paddy@0 | 315 return |
| paddy@0 | 316 } |
| paddy@0 | 317 |
| paddy@0 | 318 // generate access token |
| paddy@0 | 319 ret := &AccessRequest{ |
| paddy@0 | 320 Scope: r.Form.Get("scope"), |
| paddy@0 | 321 AssertionType: r.Form.Get("assertion_type"), |
| paddy@0 | 322 Assertion: r.Form.Get("assertion"), |
| paddy@0 | 323 GenerateRefresh: false, // assertion should NOT generate a refresh token, per the RFC |
| paddy@0 | 324 Expiration: ctx.Config.AccessExpiration, |
| paddy@0 | 325 } |
| paddy@0 | 326 |
| paddy@0 | 327 // "assertion_type" and "assertion" is required |
| paddy@0 | 328 if ret.AssertionType == "" || ret.Assertion == "" { |
| paddy@0 | 329 // TODO: return error |
| paddy@0 | 330 return |
| paddy@0 | 331 } |
| paddy@0 | 332 |
| paddy@0 | 333 // must have a valid client |
| paddy@0 | 334 ret.Client, err = getClient(auth, ctx) |
| paddy@0 | 335 if err != nil { |
| paddy@0 | 336 //TODO: return error |
| paddy@0 | 337 return |
| paddy@0 | 338 } |
| paddy@0 | 339 |
| paddy@0 | 340 // set redirect uri |
| paddy@0 | 341 ret.RedirectURI = ret.Client.RedirectURI |
| paddy@0 | 342 |
| paddy@0 | 343 // TODO: write ret |
| paddy@0 | 344 } |
| paddy@0 | 345 |
| paddy@0 | 346 func FinishAccessRequest(w http.ResponseWriter, r *http.Request, ar AccessRequest, ctx Context) { |
| paddy@0 | 347 // TODO: check if authorized? |
| paddy@0 | 348 redirectURI := r.Form.Get("redirect_uri") |
| paddy@0 | 349 // Get redirect uri from AccessRequest if it's there (e.g., refresh token request) |
| paddy@0 | 350 if ar.RedirectURI != "" { |
| paddy@0 | 351 redirectURI = ar.RedirectURI |
| paddy@0 | 352 } |
| paddy@0 | 353 ret := AccessData{ |
| paddy@0 | 354 Client: ar.Client, |
| paddy@0 | 355 AuthorizeData: &ar.AuthorizeData, |
| paddy@0 | 356 AccessData: &ar.AccessData, |
| paddy@0 | 357 RedirectURI: redirectURI, |
| paddy@0 | 358 CreatedAt: time.Now(), |
| paddy@0 | 359 ExpiresIn: ar.Expiration, |
| paddy@0 | 360 Scope: ar.Scope, |
| paddy@0 | 361 } |
| paddy@0 | 362 |
| paddy@0 | 363 var err error |
| paddy@0 | 364 |
| paddy@0 | 365 // generate access token |
| paddy@0 | 366 ret.AccessToken = newToken() |
| paddy@0 | 367 if ar.GenerateRefresh { |
| paddy@0 | 368 ret.RefreshToken = newToken() |
| paddy@0 | 369 } |
| paddy@0 | 370 |
| paddy@0 | 371 // save access token |
| paddy@0 | 372 err = saveAccess(ret, ctx) |
| paddy@0 | 373 if err != nil { |
| paddy@0 | 374 // TODO: return error |
| paddy@0 | 375 return |
| paddy@0 | 376 } |
| paddy@0 | 377 |
| paddy@0 | 378 // remove authorization token |
| paddy@0 | 379 if ret.AuthorizeData != nil { |
| paddy@0 | 380 err = removeAuthorize(ret.AuthorizeData.Code, ctx) |
| paddy@0 | 381 if err != nil { |
| paddy@0 | 382 // TODO: log error |
| paddy@0 | 383 } |
| paddy@0 | 384 } |
| paddy@0 | 385 |
| paddy@0 | 386 // remove previous access token |
| paddy@0 | 387 if ret.AccessData != nil { |
| paddy@0 | 388 if ret.AccessData.RefreshToken != "" { |
| paddy@0 | 389 err = removeRefresh(ret.AccessData.RefreshToken, ctx) |
| paddy@0 | 390 if err != nil { |
| paddy@0 | 391 // TODO: log error |
| paddy@0 | 392 } |
| paddy@0 | 393 } |
| paddy@0 | 394 err = removeAccess(ret.AccessData.AccessToken, ctx) |
| paddy@0 | 395 if err != nil { |
| paddy@0 | 396 // TODO: log error |
| paddy@0 | 397 } |
| paddy@0 | 398 } |
| paddy@0 | 399 |
| paddy@0 | 400 // output data |
| paddy@0 | 401 //w.Output["access_token"] = ret.AccessToken |
| paddy@0 | 402 //w.Output["token_type"] = ctx.Config.TokenType |
| paddy@0 | 403 //w.Output["expires_in"] = ret.ExpiresIn |
| paddy@0 | 404 //if ret.RefreshToken != "" { |
| paddy@0 | 405 // w.Output["refresh_token"] = ret.RefreshToken |
| paddy@0 | 406 //} |
| paddy@0 | 407 //if ar.Scope != "" { |
| paddy@0 | 408 // w.Output["scope"] = ar.Scope |
| paddy@0 | 409 //} |
| paddy@0 | 410 // TODO: write ret |
| paddy@0 | 411 } |
| paddy@0 | 412 |
| paddy@0 | 413 // Helper Functions |
| paddy@0 | 414 |
| paddy@0 | 415 // getClient looks up and authenticates the basic auth using the given |
| paddy@0 | 416 // storage. Sets an error on the response if auth fails or a server error occurs. |
| paddy@0 | 417 func getClient(auth BasicAuth, ctx Context) (Client, error) { |
| paddy@0 | 418 id, err := uuid.Parse(auth.Username) |
| paddy@0 | 419 if err != nil { |
| paddy@0 | 420 return Client{}, err |
| paddy@0 | 421 } |
| paddy@0 | 422 client, err := GetClient(id, ctx) |
| paddy@0 | 423 if err != nil { |
| paddy@0 | 424 // TODO: abstract out errors |
| paddy@0 | 425 return Client{}, err |
| paddy@0 | 426 } |
| paddy@0 | 427 if client.Secret != auth.Password { |
| paddy@0 | 428 // TODO: return E_UNAUTHORIZED_CLIENT error |
| paddy@0 | 429 return Client{}, nil |
| paddy@0 | 430 } |
| paddy@0 | 431 if client.RedirectURI == "" { |
| paddy@0 | 432 // TODO: return E_UNAUTHORIZED_CLIENT error |
| paddy@0 | 433 return Client{}, nil |
| paddy@0 | 434 } |
| paddy@0 | 435 return client, nil |
| paddy@0 | 436 } |
| paddy@0 | 437 |
| paddy@0 | 438 func loadRefresh(code string, ctx Context) (AccessData, error) { |
| paddy@0 | 439 return AccessData{}, nil |
| paddy@0 | 440 } |
| paddy@0 | 441 |
| paddy@0 | 442 func loadAccess(code string, ctx Context) (AccessData, error) { |
| paddy@0 | 443 return AccessData{}, nil |
| paddy@0 | 444 } |
| paddy@0 | 445 |
| paddy@0 | 446 func saveAccess(data AccessData, ctx Context) error { |
| paddy@0 | 447 return nil |
| paddy@0 | 448 } |
| paddy@0 | 449 |
| paddy@0 | 450 func removeAccess(token string, ctx Context) error { |
| paddy@0 | 451 return nil |
| paddy@0 | 452 } |
| paddy@0 | 453 |
| paddy@0 | 454 func removeRefresh(token string, ctx Context) error { |
| paddy@0 | 455 return nil |
| paddy@0 | 456 } |