auth
2014-09-01
auth/access.go.old
Deprecate old implementations. Let's remove all of the osin stuff altogether, in favour of a more testable, unit-based approach. Leave all the old files around, for easy reference, but add the .old suffix so the go tools don't pick them up.
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/access.go.old Mon Sep 01 09:13:52 2014 -0400 1.3 @@ -0,0 +1,415 @@ 1.4 +package auth 1.5 + 1.6 +import ( 1.7 + "net/http" 1.8 + "net/url" 1.9 + "time" 1.10 + 1.11 + "strconv" 1.12 + "secondbit.org/uuid" 1.13 +) 1.14 + 1.15 +// GrantType is the type for OAuth param `grant_type` 1.16 +type GrantType string 1.17 + 1.18 +const ( 1.19 + AuthorizationCodeGrant GrantType = "authorization_code" 1.20 + RefreshTokenGrant = "refresh_token" 1.21 + PasswordGrant = "password" 1.22 + ClientCredentialsGrant = "client_credentials" 1.23 +) 1.24 + 1.25 +// AccessData represents an access grant (tokens, expiration, client, etc) 1.26 +type AccessData struct { 1.27 + PreviousAuthorizeData *AuthorizeData `json:"-"` 1.28 + PreviousAccessData *AccessData `json:"-"` // previous access data, when refreshing 1.29 + AccessToken string `json:"access_token"` 1.30 + RefreshToken string `json:"refresh_token,omitempty"` 1.31 + ExpiresIn int32 `json:"expires_in"` 1.32 + CreatedAt time.Time `json:"-"` 1.33 + TokenType string `json:"token_type"` 1.34 + Scope string `json:"scope,omitempty"` 1.35 + ProfileID uuid.ID `json:"-"` 1.36 + AuthRequest `json:"-"` 1.37 +} 1.38 + 1.39 +// IsExpired returns true if access expired 1.40 +func (d *AccessData) IsExpired() bool { 1.41 + return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second).Before(time.Now()) 1.42 +} 1.43 + 1.44 +// ExpireAt returns the expiration date 1.45 +func (d *AccessData) ExpireAt() time.Time { 1.46 + return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second) 1.47 +} 1.48 + 1.49 +// HandleOAuth2AccessRequest is the http.HandlerFunc for handling access token requests. 1.50 +func HandleOAuth2AccessRequest(w http.ResponseWriter, r *http.Request, ctx Context) { 1.51 + // Only allow GET or POST 1.52 + if r.Method != "POST" { 1.53 + if r.Method != "GET" || !ctx.Config.AllowGetAccessRequest { 1.54 + ctx.RenderJSONError(w, ErrorInvalidRequest, "Invalid request method.", ctx.Config.DocumentationDomain) 1.55 + return 1.56 + } 1.57 + } 1.58 + 1.59 + grantType := GrantType(r.Form.Get("grant_type")) 1.60 + if ctx.Config.AllowedAccessTypes.Exists(grantType) { 1.61 + switch grantType { 1.62 + case AuthorizationCodeGrant: 1.63 + handleAuthorizationCodeRequest(w, r, ctx) 1.64 + case RefreshTokenGrant: 1.65 + handleRefreshTokenRequest(w, r, ctx) 1.66 + case PasswordGrant: 1.67 + handlePasswordRequest(w, r, ctx) 1.68 + case ClientCredentialsGrant: 1.69 + handleClientCredentialsRequest(w, r, ctx) 1.70 + default: 1.71 + ctx.RenderJSONError(w, ErrorUnsupportedGrantType, "Unsupported grant type.", ctx.Config.DocumentationDomain) 1.72 + return 1.73 + } 1.74 + } 1.75 +} 1.76 + 1.77 +func handleAuthorizationCodeRequest(w http.ResponseWriter, r *http.Request, ctx Context) { 1.78 + // get client authentication 1.79 + auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) 1.80 + if err != nil { 1.81 + ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) 1.82 + return 1.83 + } 1.84 + 1.85 + code := r.Form.Get("code") 1.86 + // "code" is required 1.87 + if code == "" { 1.88 + ctx.RenderJSONError(w, ErrorInvalidRequest, "Code must be supplied.", ctx.Config.DocumentationDomain) 1.89 + return 1.90 + } 1.91 + 1.92 + // must have a valid client 1.93 + client, err := getClient(auth, ctx) 1.94 + if err != nil { 1.95 + if err == ClientNotFoundError || err == InvalidClientError { 1.96 + ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) 1.97 + return 1.98 + } 1.99 + ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) 1.100 + return 1.101 + } 1.102 + 1.103 + // must be a valid authorization code 1.104 + authData, err := ctx.Tokens.GetAuthorization(code) 1.105 + if err != nil { 1.106 + if err == AuthorizationNotFoundError { 1.107 + ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid authorization.", ctx.Config.DocumentationDomain) 1.108 + return 1.109 + } 1.110 + ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) 1.111 + return 1.112 + } 1.113 + if authData.RedirectURI == "" { 1.114 + ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid redirect on grant.", ctx.Config.DocumentationDomain) 1.115 + return 1.116 + } 1.117 + if authData.IsExpired() { 1.118 + ctx.RenderJSONError(w, ErrorInvalidGrant, "Authorization is expired.", ctx.Config.DocumentationDomain) 1.119 + return 1.120 + } 1.121 + 1.122 + // code must be from the client 1.123 + if !authData.Client.ID.Equal(client.ID) { 1.124 + ctx.RenderJSONError(w, ErrorInvalidGrant, "Grant issued to another client.", ctx.Config.DocumentationDomain) 1.125 + return 1.126 + } 1.127 + 1.128 + // check redirect uri 1.129 + redirectURI := r.Form.Get("redirect_uri") 1.130 + if redirectURI == "" { 1.131 + redirectURI = client.RedirectURI 1.132 + } 1.133 + if err = validateURI(client.RedirectURI, redirectURI); err != nil { 1.134 + ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match client.", ctx.Config.DocumentationDomain) 1.135 + return 1.136 + } 1.137 + if authData.RedirectURI != redirectURI { 1.138 + ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match auth redirect.", ctx.Config.DocumentationDomain) 1.139 + return 1.140 + } 1.141 + 1.142 + data := AccessData{ 1.143 + AuthRequest: AuthRequest{ 1.144 + Client: client, 1.145 + RedirectURI: redirectURI, 1.146 + Scope: authData.Scope, 1.147 + }, 1.148 + Scope: authData.Scope, 1.149 + PreviousAuthorizeData: &authData, 1.150 + } 1.151 + 1.152 + err = fillTokens(&data, true, ctx) 1.153 + if err != nil { 1.154 + ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) 1.155 + return 1.156 + } 1.157 + ctx.RenderJSONToken(w, data) 1.158 +} 1.159 + 1.160 +func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) { 1.161 + // get client authentication 1.162 + auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) 1.163 + 1.164 + if err != nil { 1.165 + ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) 1.166 + return 1.167 + } 1.168 + 1.169 + code := r.Form.Get("refresh_token") 1.170 + 1.171 + // "refresh_token" is required 1.172 + if code == "" { 1.173 + ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing refresh token.", ctx.Config.DocumentationDomain) 1.174 + return 1.175 + } 1.176 + 1.177 + // must have a valid client 1.178 + client, err := getClient(auth, ctx) 1.179 + if err != nil { 1.180 + if err == ClientNotFoundError || err == InvalidClientError { 1.181 + ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) 1.182 + return 1.183 + } 1.184 + ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) 1.185 + return 1.186 + } 1.187 + 1.188 + // must be a valid refresh code 1.189 + refreshData, err := ctx.Tokens.GetRefresh(code) 1.190 + if err != nil { 1.191 + if err == TokenNotFoundError { 1.192 + ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token not valid.", ctx.Config.DocumentationDomain) 1.193 + return 1.194 + } 1.195 + ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) 1.196 + return 1.197 + } 1.198 + 1.199 + // client must be the same as the previous token 1.200 + if !refreshData.Client.ID.Equal(client.ID) { 1.201 + ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token issued to another client.", ctx.Config.DocumentationDomain) 1.202 + return 1.203 + } 1.204 + 1.205 + scope := r.Form.Get("scope") 1.206 + if scope == "" { 1.207 + scope = refreshData.Scope 1.208 + } 1.209 + 1.210 + data := AccessData{ 1.211 + AuthRequest: AuthRequest{ 1.212 + Client: client, 1.213 + Scope: scope, 1.214 + }, 1.215 + Scope: scope, 1.216 + PreviousAccessData: &refreshData, 1.217 + } 1.218 + err = fillTokens(&data, true, ctx) 1.219 + if err != nil { 1.220 + ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) 1.221 + return 1.222 + } 1.223 + ctx.RenderJSONToken(w, data) 1.224 +} 1.225 + 1.226 +func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) { 1.227 + // get client authentication 1.228 + auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) 1.229 + if err != nil { 1.230 + ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) 1.231 + return 1.232 + } 1.233 + 1.234 + username := r.Form.Get("username") 1.235 + password := r.Form.Get("password") 1.236 + scope := r.Form.Get("scope") 1.237 + 1.238 + // "username" and "password" is required 1.239 + if username == "" || password == "" { 1.240 + ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing credentials.", ctx.Config.DocumentationDomain) 1.241 + return 1.242 + } 1.243 + 1.244 + // must have a valid client 1.245 + client, err := getClient(auth, ctx) 1.246 + if err != nil { 1.247 + if err == ClientNotFoundError || err == InvalidClientError { 1.248 + ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) 1.249 + return 1.250 + } 1.251 + ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) 1.252 + return 1.253 + } 1.254 + 1.255 + _, err = ctx.Profiles.GetProfile(username, password) 1.256 + if err != nil { 1.257 + if err == ErrProfileNotFound { 1.258 + ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid credentials.", ctx.Config.DocumentationDomain) 1.259 + return 1.260 + } 1.261 + ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) 1.262 + return 1.263 + } 1.264 + 1.265 + data := AccessData{ 1.266 + AuthRequest: AuthRequest{ 1.267 + Client: client, 1.268 + Scope: scope, 1.269 + }, 1.270 + Scope: scope, 1.271 + } 1.272 + 1.273 + err = fillTokens(&data, true, ctx) 1.274 + if err != nil { 1.275 + ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) 1.276 + return 1.277 + } 1.278 + ctx.RenderJSONToken(w, data) 1.279 +} 1.280 + 1.281 +func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) { 1.282 + // get client authentication 1.283 + auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) 1.284 + if err != nil { 1.285 + ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) 1.286 + return 1.287 + } 1.288 + 1.289 + scope := r.Form.Get("scope") 1.290 + 1.291 + // must have a valid client 1.292 + client, err := getClient(auth, ctx) 1.293 + if err != nil { 1.294 + if err == ClientNotFoundError || err == InvalidClientError { 1.295 + ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) 1.296 + return 1.297 + } 1.298 + ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) 1.299 + return 1.300 + } 1.301 + 1.302 + data := AccessData{ 1.303 + AuthRequest: AuthRequest{ 1.304 + Client: client, 1.305 + Scope: scope, 1.306 + }, 1.307 + Scope: scope, 1.308 + } 1.309 + 1.310 + err = fillTokens(&data, true, ctx) 1.311 + if err != nil { 1.312 + ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) 1.313 + return 1.314 + } 1.315 + ctx.RenderJSONToken(w, data) 1.316 +} 1.317 + 1.318 +func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error { 1.319 + var err error 1.320 + 1.321 + // generate access token 1.322 + data.AccessToken = newToken() 1.323 + if includeRefresh { 1.324 + data.RefreshToken = newToken() 1.325 + } 1.326 + 1.327 + // save access token 1.328 + err = ctx.Tokens.SaveAccess(*data) 1.329 + if err != nil { 1.330 + if ctx.Log != nil { 1.331 + ctx.Log.Printf("Error writing access token: %s\n", err) 1.332 + } 1.333 + return InternalServerError 1.334 + } 1.335 + 1.336 + // remove authorization token 1.337 + if data.PreviousAuthorizeData != nil { 1.338 + err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code) 1.339 + if err != nil && ctx.Log != nil { 1.340 + ctx.Log.Printf("Error removing previous auth data (%s): %s\n", data.PreviousAuthorizeData.Code, err) 1.341 + } 1.342 + } 1.343 + 1.344 + // remove previous access token 1.345 + if data.PreviousAccessData != nil { 1.346 + if data.PreviousAccessData.RefreshToken != "" { 1.347 + err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken) 1.348 + if err != nil && ctx.Log != nil { 1.349 + ctx.Log.Printf("Error removing previous refresh token (%s): %s\n", data.PreviousAccessData.RefreshToken, err) 1.350 + } 1.351 + } 1.352 + err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken) 1.353 + if err != nil && ctx.Log != nil { 1.354 + ctx.Log.Printf("Error removing previous access token (%s): %s\n", data.PreviousAccessData.AccessToken, err) 1.355 + } 1.356 + } 1.357 + 1.358 + data.TokenType = ctx.Config.TokenType 1.359 + data.ExpiresIn = ctx.Config.AccessExpiration 1.360 + data.CreatedAt = time.Now() 1.361 + return nil 1.362 +} 1.363 + 1.364 +func (data AccessData) GetRedirect(fragment bool) (string, error) { 1.365 + u, err := url.Parse(data.RedirectURI) 1.366 + if err != nil { 1.367 + return "", err 1.368 + } 1.369 + 1.370 + // add parameters 1.371 + q := u.Query() 1.372 + q.Set("access_token", data.AccessToken) 1.373 + q.Set("token_type", data.TokenType) 1.374 + q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10)) 1.375 + if data.RefreshToken != "" { 1.376 + q.Set("refresh_token", data.RefreshToken) 1.377 + } 1.378 + if data.Scope != "" { 1.379 + q.Set("scope", data.Scope) 1.380 + } 1.381 + if len(data.ProfileID) > 0 { 1.382 + q.Set("profile", data.ProfileID.String()) 1.383 + } 1.384 + if fragment { 1.385 + u.RawQuery = "" 1.386 + u.Fragment = q.Encode() 1.387 + } else { 1.388 + u.RawQuery = q.Encode() 1.389 + } 1.390 + 1.391 + return u.String(), nil 1.392 +} 1.393 + 1.394 +// getClient looks up and authenticates the basic auth using the given 1.395 +// storage. Sets an error on the response if auth fails or a server error occurs. 1.396 +func getClient(auth BasicAuth, ctx Context) (Client, error) { 1.397 + id, err := uuid.Parse(auth.Username) 1.398 + if err != nil { 1.399 + return Client{}, err 1.400 + } 1.401 + client, err := ctx.Clients.GetClient(id) 1.402 + if err != nil { 1.403 + if err == ClientNotFoundError { 1.404 + return Client{}, err 1.405 + } 1.406 + if ctx.Log != nil { 1.407 + ctx.Log.Printf("Error retrieving client %s: %s", id, err) 1.408 + } 1.409 + return Client{}, InternalServerError 1.410 + } 1.411 + if client.Secret != auth.Password { 1.412 + return Client{}, InvalidClientError 1.413 + } 1.414 + if client.RedirectURI == "" { 1.415 + return Client{}, InvalidClientError 1.416 + } 1.417 + return client, nil 1.418 +}