auth

Paddy 2014-07-18 Child:7b9e0fc20256

0:7a6f64db7246 Go to Latest

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.

History
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 }