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.
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/access.go Fri Jul 18 07:13:22 2014 -0400 1.3 @@ -0,0 +1,456 @@ 1.4 +package oauth2 1.5 + 1.6 +import ( 1.7 + "net/http" 1.8 + "time" 1.9 + 1.10 + "secondbit.org/uuid" 1.11 +) 1.12 + 1.13 +// GrantType is the type for OAuth param `grant_type` 1.14 +type GrantType string 1.15 + 1.16 +const ( 1.17 + AuthorizationCodeGrant GrantType = "authorization_code" 1.18 + RefreshTokenGrant = "refresh_token" 1.19 + PasswordGrant = "password" 1.20 + ClientCredentialsGrant = "client_credentials" 1.21 + AssertionGrant = "assertion" 1.22 + ImplicitGrant = "__implicit" 1.23 +) 1.24 + 1.25 +// AccessRequest is a request for access tokens 1.26 +type AccessRequest struct { 1.27 + Code string 1.28 + Client Client 1.29 + AuthorizeData AuthorizeData 1.30 + AccessData AccessData 1.31 + RedirectURI string 1.32 + Scope string 1.33 + Username string 1.34 + Password string 1.35 + AssertionType string 1.36 + Assertion string 1.37 + 1.38 + // Token expiration in seconds. Change if different from default 1.39 + Expiration int32 1.40 + 1.41 + // Set if a refresh token should be generated 1.42 + GenerateRefresh bool 1.43 +} 1.44 + 1.45 +// AccessData represents an access grant (tokens, expiration, client, etc) 1.46 +type AccessData struct { 1.47 + // Client information 1.48 + Client Client 1.49 + 1.50 + // Authorize data, for authorization code 1.51 + AuthorizeData *AuthorizeData 1.52 + 1.53 + // Previous access data, for refresh token 1.54 + AccessData *AccessData 1.55 + 1.56 + // Access token 1.57 + AccessToken string 1.58 + 1.59 + // Refresh Token. Can be blank 1.60 + RefreshToken string 1.61 + 1.62 + // Token expiration in seconds 1.63 + ExpiresIn int32 1.64 + 1.65 + // Requested scope 1.66 + Scope string 1.67 + 1.68 + // Redirect URI from request 1.69 + RedirectURI string 1.70 + 1.71 + // Date created 1.72 + CreatedAt time.Time 1.73 +} 1.74 + 1.75 +// IsExpired returns true if access expired 1.76 +func (d *AccessData) IsExpired() bool { 1.77 + return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second).Before(time.Now()) 1.78 +} 1.79 + 1.80 +// ExpireAt returns the expiration date 1.81 +func (d *AccessData) ExpireAt() time.Time { 1.82 + return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second) 1.83 +} 1.84 + 1.85 +// AccessTokenGen generates access tokens 1.86 +type AccessTokenGen interface { 1.87 + GenerateAccessToken(data *AccessData, generaterefresh bool) (accesstoken string, refreshtoken string, err error) 1.88 +} 1.89 + 1.90 +// HandleOAuth2AccessRequest is the http.HandlerFunc for handling access token requests. 1.91 +func HandleOAuth2AccessRequest(w http.ResponseWriter, r *http.Request, ctx Context) { 1.92 + // Only allow GET or POST 1.93 + if r.Method != "POST" { 1.94 + if r.Method == "GET" && !ctx.Config.AllowGetAccessRequest { 1.95 + // TODO: return error 1.96 + return 1.97 + } 1.98 + } 1.99 + 1.100 + err := r.ParseForm() 1.101 + if err != nil { 1.102 + // TODO: return error 1.103 + return 1.104 + } 1.105 + 1.106 + grantType := GrantType(r.Form.Get("grant_type")) 1.107 + if ctx.Config.AllowedAccessTypes.Exists(grantType) { 1.108 + switch grantType { 1.109 + case AuthorizationCodeGrant: 1.110 + handleAuthorizationCodeRequest(w, r, ctx) 1.111 + case RefreshTokenGrant: 1.112 + handleRefreshTokenRequest(w, r, ctx) 1.113 + case PasswordGrant: 1.114 + handlePasswordRequest(w, r, ctx) 1.115 + case ClientCredentialsGrant: 1.116 + handleClientCredentialsRequest(w, r, ctx) 1.117 + case AssertionGrant: 1.118 + handleAssertionRequest(w, r, ctx) 1.119 + default: 1.120 + // TODO: return error 1.121 + return 1.122 + } 1.123 + } 1.124 +} 1.125 + 1.126 +func handleAuthorizationCodeRequest(w http.ResponseWriter, r *http.Request, ctx Context) { 1.127 + // get client authentication 1.128 + auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) 1.129 + if err != nil { 1.130 + // TODO: return error 1.131 + return 1.132 + } 1.133 + 1.134 + // generate access token 1.135 + ret := AccessRequest{ 1.136 + Code: r.Form.Get("code"), 1.137 + RedirectURI: r.Form.Get("redirect_uri"), 1.138 + GenerateRefresh: true, 1.139 + Expiration: ctx.Config.AccessExpiration, 1.140 + } 1.141 + 1.142 + // "code" is required 1.143 + if ret.Code == "" { 1.144 + // TODO: return error 1.145 + return 1.146 + } 1.147 + 1.148 + // must have a valid client 1.149 + ret.Client, err = getClient(auth, ctx) 1.150 + if err != nil { 1.151 + // TODO: return error 1.152 + return 1.153 + } 1.154 + 1.155 + // must be a valid authorization code 1.156 + ret.AuthorizeData, err = loadAuthorize(ret.Code, ctx) 1.157 + if err != nil { 1.158 + // TODO: return error 1.159 + return 1.160 + } 1.161 + if ret.AuthorizeData.Client.RedirectURI == "" { 1.162 + // TODO: return error 1.163 + return 1.164 + } 1.165 + if ret.AuthorizeData.IsExpired() { 1.166 + return // TODO: return error 1.167 + } 1.168 + 1.169 + // code must be from the client 1.170 + if !ret.AuthorizeData.Client.ID.Equal(ret.Client.ID) { 1.171 + // TODO: return error 1.172 + return 1.173 + } 1.174 + 1.175 + // check redirect uri 1.176 + if ret.RedirectURI == "" { 1.177 + ret.RedirectURI = ret.Client.RedirectURI 1.178 + } 1.179 + if err = ValidateURI(ret.Client.RedirectURI, ret.RedirectURI); err != nil { 1.180 + // TODO: return error 1.181 + return 1.182 + } 1.183 + if ret.AuthorizeData.RedirectURI != ret.RedirectURI { 1.184 + // TODO: return error 1.185 + return 1.186 + } 1.187 + 1.188 + // set rest of data 1.189 + ret.Scope = ret.AuthorizeData.Scope 1.190 + // TODO: write ret 1.191 +} 1.192 + 1.193 +func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) { 1.194 + // get client authentication 1.195 + auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) 1.196 + if err != nil { 1.197 + // TODO: return error 1.198 + return 1.199 + } 1.200 + 1.201 + // generate access token 1.202 + ret := AccessRequest{ 1.203 + Code: r.Form.Get("refresh_token"), 1.204 + Scope: r.Form.Get("scope"), 1.205 + GenerateRefresh: true, 1.206 + Expiration: ctx.Config.AccessExpiration, 1.207 + } 1.208 + 1.209 + // "refresh_token" is required 1.210 + if ret.Code == "" { 1.211 + // TODO: return error 1.212 + return 1.213 + } 1.214 + 1.215 + // must have a valid client 1.216 + ret.Client, err = getClient(auth, ctx) 1.217 + if err != nil { 1.218 + // TODO: return error 1.219 + return 1.220 + } 1.221 + 1.222 + // must be a valid refresh code 1.223 + ret.AccessData, err = loadRefresh(ret.Code, ctx) 1.224 + if err != nil { 1.225 + // TODO: return error 1.226 + return 1.227 + } 1.228 + if ret.AccessData.Client.RedirectURI == "" { 1.229 + // TODO: return error 1.230 + return 1.231 + } 1.232 + 1.233 + // client must be the same as the previous token 1.234 + if !ret.AccessData.Client.ID.Equal(ret.Client.ID) { 1.235 + // TODO: return error 1.236 + return 1.237 + 1.238 + } 1.239 + 1.240 + // set rest of data 1.241 + ret.RedirectURI = ret.AccessData.RedirectURI 1.242 + if ret.Scope == "" { 1.243 + ret.Scope = ret.AccessData.Scope 1.244 + } 1.245 + 1.246 + // TODO: write ret 1.247 +} 1.248 + 1.249 +func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) { 1.250 + // get client authentication 1.251 + auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) 1.252 + if err != nil { 1.253 + // TODO: return error 1.254 + return 1.255 + } 1.256 + 1.257 + // generate access token 1.258 + ret := AccessRequest{ 1.259 + Username: r.Form.Get("username"), 1.260 + Password: r.Form.Get("password"), 1.261 + Scope: r.Form.Get("scope"), 1.262 + GenerateRefresh: true, 1.263 + Expiration: ctx.Config.AccessExpiration, 1.264 + } 1.265 + 1.266 + // "username" and "password" is required 1.267 + if ret.Username == "" || ret.Password == "" { 1.268 + // TODO: return error 1.269 + return 1.270 + } 1.271 + 1.272 + // must have a valid client 1.273 + ret.Client, err = getClient(auth, ctx) 1.274 + if err != nil { 1.275 + // TODO: return error 1.276 + return 1.277 + } 1.278 + 1.279 + // set redirect uri 1.280 + ret.RedirectURI = ret.Client.RedirectURI 1.281 + 1.282 + // TODO: write ret 1.283 +} 1.284 + 1.285 +func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) { 1.286 + // get client authentication 1.287 + auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) 1.288 + if err != nil { 1.289 + // TODO: return error 1.290 + return 1.291 + } 1.292 + 1.293 + // generate access token 1.294 + ret := AccessRequest{ 1.295 + Scope: r.Form.Get("scope"), 1.296 + GenerateRefresh: true, 1.297 + Expiration: ctx.Config.AccessExpiration, 1.298 + } 1.299 + 1.300 + // must have a valid client 1.301 + ret.Client, err = getClient(auth, ctx) 1.302 + if err != nil { 1.303 + // TODO: return error 1.304 + return 1.305 + } 1.306 + 1.307 + // set redirect uri 1.308 + ret.RedirectURI = ret.Client.RedirectURI 1.309 + 1.310 + // TODO: write ret 1.311 +} 1.312 + 1.313 +func handleAssertionRequest(w http.ResponseWriter, r *http.Request, ctx Context) { 1.314 + // get client authentication 1.315 + auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) 1.316 + if err != nil { 1.317 + // TODO: return error 1.318 + return 1.319 + } 1.320 + 1.321 + // generate access token 1.322 + ret := &AccessRequest{ 1.323 + Scope: r.Form.Get("scope"), 1.324 + AssertionType: r.Form.Get("assertion_type"), 1.325 + Assertion: r.Form.Get("assertion"), 1.326 + GenerateRefresh: false, // assertion should NOT generate a refresh token, per the RFC 1.327 + Expiration: ctx.Config.AccessExpiration, 1.328 + } 1.329 + 1.330 + // "assertion_type" and "assertion" is required 1.331 + if ret.AssertionType == "" || ret.Assertion == "" { 1.332 + // TODO: return error 1.333 + return 1.334 + } 1.335 + 1.336 + // must have a valid client 1.337 + ret.Client, err = getClient(auth, ctx) 1.338 + if err != nil { 1.339 + //TODO: return error 1.340 + return 1.341 + } 1.342 + 1.343 + // set redirect uri 1.344 + ret.RedirectURI = ret.Client.RedirectURI 1.345 + 1.346 + // TODO: write ret 1.347 +} 1.348 + 1.349 +func FinishAccessRequest(w http.ResponseWriter, r *http.Request, ar AccessRequest, ctx Context) { 1.350 + // TODO: check if authorized? 1.351 + redirectURI := r.Form.Get("redirect_uri") 1.352 + // Get redirect uri from AccessRequest if it's there (e.g., refresh token request) 1.353 + if ar.RedirectURI != "" { 1.354 + redirectURI = ar.RedirectURI 1.355 + } 1.356 + ret := AccessData{ 1.357 + Client: ar.Client, 1.358 + AuthorizeData: &ar.AuthorizeData, 1.359 + AccessData: &ar.AccessData, 1.360 + RedirectURI: redirectURI, 1.361 + CreatedAt: time.Now(), 1.362 + ExpiresIn: ar.Expiration, 1.363 + Scope: ar.Scope, 1.364 + } 1.365 + 1.366 + var err error 1.367 + 1.368 + // generate access token 1.369 + ret.AccessToken = newToken() 1.370 + if ar.GenerateRefresh { 1.371 + ret.RefreshToken = newToken() 1.372 + } 1.373 + 1.374 + // save access token 1.375 + err = saveAccess(ret, ctx) 1.376 + if err != nil { 1.377 + // TODO: return error 1.378 + return 1.379 + } 1.380 + 1.381 + // remove authorization token 1.382 + if ret.AuthorizeData != nil { 1.383 + err = removeAuthorize(ret.AuthorizeData.Code, ctx) 1.384 + if err != nil { 1.385 + // TODO: log error 1.386 + } 1.387 + } 1.388 + 1.389 + // remove previous access token 1.390 + if ret.AccessData != nil { 1.391 + if ret.AccessData.RefreshToken != "" { 1.392 + err = removeRefresh(ret.AccessData.RefreshToken, ctx) 1.393 + if err != nil { 1.394 + // TODO: log error 1.395 + } 1.396 + } 1.397 + err = removeAccess(ret.AccessData.AccessToken, ctx) 1.398 + if err != nil { 1.399 + // TODO: log error 1.400 + } 1.401 + } 1.402 + 1.403 + // output data 1.404 + //w.Output["access_token"] = ret.AccessToken 1.405 + //w.Output["token_type"] = ctx.Config.TokenType 1.406 + //w.Output["expires_in"] = ret.ExpiresIn 1.407 + //if ret.RefreshToken != "" { 1.408 + // w.Output["refresh_token"] = ret.RefreshToken 1.409 + //} 1.410 + //if ar.Scope != "" { 1.411 + // w.Output["scope"] = ar.Scope 1.412 + //} 1.413 + // TODO: write ret 1.414 +} 1.415 + 1.416 +// Helper Functions 1.417 + 1.418 +// getClient looks up and authenticates the basic auth using the given 1.419 +// storage. Sets an error on the response if auth fails or a server error occurs. 1.420 +func getClient(auth BasicAuth, ctx Context) (Client, error) { 1.421 + id, err := uuid.Parse(auth.Username) 1.422 + if err != nil { 1.423 + return Client{}, err 1.424 + } 1.425 + client, err := GetClient(id, ctx) 1.426 + if err != nil { 1.427 + // TODO: abstract out errors 1.428 + return Client{}, err 1.429 + } 1.430 + if client.Secret != auth.Password { 1.431 + // TODO: return E_UNAUTHORIZED_CLIENT error 1.432 + return Client{}, nil 1.433 + } 1.434 + if client.RedirectURI == "" { 1.435 + // TODO: return E_UNAUTHORIZED_CLIENT error 1.436 + return Client{}, nil 1.437 + } 1.438 + return client, nil 1.439 +} 1.440 + 1.441 +func loadRefresh(code string, ctx Context) (AccessData, error) { 1.442 + return AccessData{}, nil 1.443 +} 1.444 + 1.445 +func loadAccess(code string, ctx Context) (AccessData, error) { 1.446 + return AccessData{}, nil 1.447 +} 1.448 + 1.449 +func saveAccess(data AccessData, ctx Context) error { 1.450 + return nil 1.451 +} 1.452 + 1.453 +func removeAccess(token string, ctx Context) error { 1.454 + return nil 1.455 +} 1.456 + 1.457 +func removeRefresh(token string, ctx Context) error { 1.458 + return nil 1.459 +}