auth
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.
| 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 } |