auth

Paddy 2014-08-01 Parent:7a6f64db7246 Child:65c49af1ed3f

1:7b9e0fc20256 Go to Latest

auth/access.go

Continue our descent to horribleness. Remove all the nonsense about "extensibility" and "clean separation of concerns", instead hardcoding connections to decisions. Remove all those "test" things that stopped passing.

History
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@0 50 // TODO: return error
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@0 67 // TODO: return error
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@0 77 // TODO: return error
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@0 84 // TODO: return error
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@1 101 if authData.Client.RedirectURI == "" {
paddy@0 102 // TODO: return error
paddy@0 103 return
paddy@0 104 }
paddy@1 105 if authData.IsExpired() {
paddy@0 106 return // TODO: return error
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@0 111 // TODO: return error
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@0 121 // TODO: return error
paddy@0 122 return
paddy@0 123 }
paddy@1 124 if authData.RedirectURI != redirectURI {
paddy@0 125 // TODO: return error
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@1 143 // TODO: write 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@0 149 if err != nil {
paddy@0 150 // TODO: return error
paddy@0 151 return
paddy@0 152 }
paddy@0 153
paddy@1 154 code := r.Form.Get("refresh_token")
paddy@0 155
paddy@0 156 // "refresh_token" is required
paddy@1 157 if code == "" {
paddy@0 158 // TODO: return error
paddy@0 159 return
paddy@0 160 }
paddy@0 161
paddy@0 162 // must have a valid client
paddy@1 163 client, err := getClient(auth, ctx)
paddy@0 164 if err != nil {
paddy@0 165 // TODO: return error
paddy@0 166 return
paddy@0 167 }
paddy@0 168
paddy@0 169 // must be a valid refresh code
paddy@1 170 refreshData, err := ctx.Tokens.GetRefresh(code)
paddy@0 171 if err != nil {
paddy@0 172 // TODO: return error
paddy@0 173 return
paddy@0 174 }
paddy@1 175 if refreshData.Client.RedirectURI == "" {
paddy@0 176 // TODO: return error
paddy@0 177 return
paddy@0 178 }
paddy@0 179
paddy@0 180 // client must be the same as the previous token
paddy@1 181 if !refreshData.Client.ID.Equal(client.ID) {
paddy@0 182 // TODO: return error
paddy@0 183 return
paddy@0 184 }
paddy@0 185
paddy@0 186 // set rest of data
paddy@1 187 redirectURI := r.Form.Get("redirect_uri")
paddy@1 188 if redirectURI == "" {
paddy@1 189 redirectURI = refreshData.RedirectURI
paddy@1 190 }
paddy@1 191 scope := r.Form.Get("scope")
paddy@1 192 if scope == "" {
paddy@1 193 scope = refreshData.Scope
paddy@0 194 }
paddy@0 195
paddy@1 196 data := AccessData{
paddy@1 197 AuthRequest: AuthRequest{
paddy@1 198 Client: client,
paddy@1 199 RedirectURI: redirectURI,
paddy@1 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@1 209 // TODO: write 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@0 216 // TODO: return error
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@0 226 // TODO: return error
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@0 233 // TODO: return error
paddy@0 234 return
paddy@0 235 }
paddy@0 236
paddy@0 237 // set redirect uri
paddy@1 238 redirectURI := r.Form.Get("redirect_uri")
paddy@1 239 if redirectURI == "" {
paddy@1 240 redirectURI = client.RedirectURI
paddy@1 241 }
paddy@0 242
paddy@1 243 _, err = ctx.Profiles.GetProfile(username, password)
paddy@1 244 if err != nil {
paddy@1 245 // TODO: return error
paddy@1 246 return
paddy@1 247 }
paddy@1 248
paddy@1 249 data := AccessData{
paddy@1 250 AuthRequest: AuthRequest{
paddy@1 251 Client: client,
paddy@1 252 RedirectURI: redirectURI,
paddy@1 253 Scope: scope,
paddy@1 254 },
paddy@1 255 }
paddy@1 256
paddy@1 257 err = fillTokens(&data, true, ctx)
paddy@1 258 if err != nil {
paddy@1 259 // TODO: return error
paddy@1 260 return
paddy@1 261 }
paddy@1 262
paddy@1 263 // TODO: write data
paddy@0 264 }
paddy@0 265
paddy@0 266 func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
paddy@0 267 // get client authentication
paddy@0 268 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
paddy@0 269 if err != nil {
paddy@0 270 // TODO: return error
paddy@0 271 return
paddy@0 272 }
paddy@0 273
paddy@1 274 scope := r.Form.Get("scope")
paddy@0 275
paddy@0 276 // must have a valid client
paddy@1 277 client, err := getClient(auth, ctx)
paddy@0 278 if err != nil {
paddy@0 279 // TODO: return error
paddy@0 280 return
paddy@0 281 }
paddy@0 282
paddy@0 283 // set redirect uri
paddy@1 284 redirectURI := r.Form.Get("redirect_uri")
paddy@1 285 if redirectURI == "" {
paddy@1 286 redirectURI = client.RedirectURI
paddy@1 287 }
paddy@0 288
paddy@1 289 data := AccessData{
paddy@1 290 AuthRequest: AuthRequest{
paddy@1 291 Client: client,
paddy@1 292 RedirectURI: redirectURI,
paddy@1 293 Scope: scope,
paddy@1 294 },
paddy@1 295 }
paddy@0 296
paddy@1 297 err = fillTokens(&data, true, ctx)
paddy@0 298 if err != nil {
paddy@0 299 // TODO: return error
paddy@0 300 return
paddy@0 301 }
paddy@0 302
paddy@1 303 // TODO: write data
paddy@0 304 }
paddy@0 305
paddy@1 306 func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error {
paddy@0 307 var err error
paddy@0 308
paddy@0 309 // generate access token
paddy@1 310 data.AccessToken = newToken()
paddy@1 311 if includeRefresh {
paddy@1 312 data.RefreshToken = newToken()
paddy@0 313 }
paddy@0 314
paddy@0 315 // save access token
paddy@1 316 err = ctx.Tokens.SaveAccess(*data)
paddy@0 317 if err != nil {
paddy@1 318 // TODO: abstract out error
paddy@1 319 return err
paddy@0 320 }
paddy@0 321
paddy@0 322 // remove authorization token
paddy@1 323 if data.PreviousAuthorizeData != nil {
paddy@1 324 err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code)
paddy@0 325 if err != nil {
paddy@0 326 // TODO: log error
paddy@0 327 }
paddy@0 328 }
paddy@0 329
paddy@0 330 // remove previous access token
paddy@1 331 if data.PreviousAccessData != nil {
paddy@1 332 if data.PreviousAccessData.RefreshToken != "" {
paddy@1 333 err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken)
paddy@0 334 if err != nil {
paddy@0 335 // TODO: log error
paddy@0 336 }
paddy@0 337 }
paddy@1 338 err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken)
paddy@0 339 if err != nil {
paddy@0 340 // TODO: log error
paddy@0 341 }
paddy@0 342 }
paddy@0 343
paddy@1 344 data.TokenType = ctx.Config.TokenType
paddy@1 345 data.ExpiresIn = ctx.Config.AccessExpiration
paddy@1 346 data.CreatedAt = time.Now()
paddy@1 347 return nil
paddy@0 348 }
paddy@0 349
paddy@1 350 func (data AccessData) GetRedirect(fragment bool) (string, error) {
paddy@1 351 u, err := url.Parse(data.RedirectURI)
paddy@1 352 if err != nil {
paddy@1 353 return "", err
paddy@1 354 }
paddy@1 355
paddy@1 356 // add parameters
paddy@1 357 q := u.Query()
paddy@1 358 q.Set("access_token", data.AccessToken)
paddy@1 359 q.Set("token_type", data.TokenType)
paddy@1 360 q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10))
paddy@1 361 if data.RefreshToken != "" {
paddy@1 362 q.Set("refresh_token", data.RefreshToken)
paddy@1 363 }
paddy@1 364 if data.Scope != "" {
paddy@1 365 q.Set("scope", data.Scope)
paddy@1 366 }
paddy@1 367 if len(data.ProfileID) > 0 {
paddy@1 368 q.Set("profile", data.ProfileID.String())
paddy@1 369 }
paddy@1 370 if fragment {
paddy@1 371 u.RawQuery = ""
paddy@1 372 u.Fragment = q.Encode()
paddy@1 373 } else {
paddy@1 374 u.RawQuery = q.Encode()
paddy@1 375 }
paddy@1 376
paddy@1 377 return u.String(), nil
paddy@1 378 }
paddy@0 379
paddy@0 380 // getClient looks up and authenticates the basic auth using the given
paddy@0 381 // storage. Sets an error on the response if auth fails or a server error occurs.
paddy@0 382 func getClient(auth BasicAuth, ctx Context) (Client, error) {
paddy@0 383 id, err := uuid.Parse(auth.Username)
paddy@0 384 if err != nil {
paddy@0 385 return Client{}, err
paddy@0 386 }
paddy@1 387 client, err := ctx.Clients.GetClient(id)
paddy@0 388 if err != nil {
paddy@0 389 // TODO: abstract out errors
paddy@0 390 return Client{}, err
paddy@0 391 }
paddy@0 392 if client.Secret != auth.Password {
paddy@0 393 // TODO: return E_UNAUTHORIZED_CLIENT error
paddy@0 394 return Client{}, nil
paddy@0 395 }
paddy@0 396 if client.RedirectURI == "" {
paddy@0 397 // TODO: return E_UNAUTHORIZED_CLIENT error
paddy@0 398 return Client{}, nil
paddy@0 399 }
paddy@0 400 return client, nil
paddy@0 401 }