# HG changeset patch # User Peter Sanchez # Date 1683667402 21600 # Tue May 09 15:23:22 2023 -0600 # Node ID e437e67f5eaf0010bc3c3a5ac9dbbe17c46d045d # Parent 5e4acf60a40172e8f24030811d400237593faf8e Working token exchange diff --git a/oauth2_authorizations.go b/authorizations.go rename from oauth2_authorizations.go rename to authorizations.go --- a/oauth2_authorizations.go +++ b/authorizations.go @@ -4,6 +4,7 @@ "context" "database/sql" "fmt" + "time" sq "github.com/Masterminds/squirrel" "hg.code.netlandish.com/~netlandish/gobwebs/database" @@ -90,3 +91,8 @@ }) return err } + +// IsExpired will check the current authorization exiration status +func (a *Authorization) IsExpired() bool { + return time.Now().UTC().After(a.CreatedOn.Add(5 * time.Minute).UTC()) +} diff --git a/oauth2_clients.go b/clients.go rename from oauth2_clients.go rename to clients.go --- a/oauth2_clients.go +++ b/clients.go @@ -4,6 +4,7 @@ "context" "crypto/rand" "crypto/sha512" + "crypto/subtle" "database/sql" "encoding/base64" "encoding/hex" @@ -110,7 +111,7 @@ } // GenerateKeys will create keys for client -func (c *Client) GenerateKeys() { +func (c *Client) GenerateKeys() string { var seed [64]byte n, err := rand.Read(seed[:]) if err != nil || n != len(seed) { @@ -124,6 +125,24 @@ c.Key = clientID.String() c.SecretHash = hex.EncodeToString(hash[:]) c.SecretPartial = partial + return secret +} + +// VerifyClientSecret will verify the secret key +func (c *Client) VerifyClientSecret(clientSecret string) bool { + wantHash, err := hex.DecodeString(c.SecretHash) + if err != nil { + panic(err) + } + + b, err := base64.StdEncoding.DecodeString(clientSecret) + if err != nil { + return false + } + gotHash := sha512.Sum512(b) + + return subtle.ConstantTimeCompare(wantHash, gotHash[:]) == 1 + } // Delete will delete this client diff --git a/oauth2_grants.go b/grants.go rename from oauth2_grants.go rename to grants.go diff --git a/logic.go b/logic.go --- a/logic.go +++ b/logic.go @@ -27,7 +27,7 @@ opts := &database.FilterOptions{ Filter: sq.And{ sq.Eq{"token_hash": hashStr}, - sq.Expr("expires > NOW() at time zone 'UTC'"), + sq.Expr("expires at time zone 'UTC' > NOW() at time zone 'UTC'"), }, } grants, err := GetGrants(ctx, opts) diff --git a/routes.go b/routes.go --- a/routes.go +++ b/routes.go @@ -3,6 +3,8 @@ import ( "crypto/rand" "crypto/sha512" + "database/sql" + "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -21,6 +23,7 @@ // ServiceConfig let's you add basic config variables to service type ServiceConfig struct { + Helper Helper DocumentationURL string Scopes []string } @@ -29,13 +32,13 @@ type Service struct { name string eg *echo.Group - helper Helper config *ServiceConfig } // RegisterRoutes ... func (s *Service) RegisterRoutes() { - s.eg.POST("/introspect", s.Introspect).Name = s.RouteName("introspect_post") + s.eg.POST("/access-token", s.AccessTokenPOST).Name = s.RouteName("access_token_post") + s.eg.POST("/introspect", s.IntrospectPOST).Name = s.RouteName("introspect_post") s.eg.Use(auth.AuthRequired()) s.eg.GET("/personal", s.ListPersonal).Name = s.RouteName("list_personal") @@ -151,17 +154,18 @@ RedirectURL: form.RedirectURL, ClientURL: form.ClientURL, } - client.GenerateKeys() + token := client.GenerateKeys() if err := client.Store(c.Request().Context()); err != nil { return err } - if s.helper != nil { + if s.config.Helper != nil { c.Set("client", client) - if err := s.helper.ProcessSuccessfulClientAdd(c); err != nil { + if err := s.config.Helper.ProcessSuccessfulClientAdd(c); err != nil { return err } } gmap["client"] = client + gmap["token"] = token return gctx.Render(http.StatusOK, "oauth2_add_client_done.html", gmap) } return gctx.Render(http.StatusOK, "oauth2_add_client.html", gmap) @@ -324,21 +328,181 @@ return oauth2Redirect(c, client.RedirectURL, gmap) } -// Introspect ... -func (s *Service) Introspect(c echo.Context) error { +func (s *Service) accessTokenError(c echo.Context, err, desc string, status int) error { + if status == 0 { + status = http.StatusBadRequest // 400 + } + retErr := struct { + Err string `json:"error"` + Desc string `json:"error_description"` + URI string `json:"error_url"` + }{ + Err: err, + Desc: desc, + URI: s.config.DocumentationURL, + } + return c.JSON(status, &retErr) +} + +// AccessTokenPOST ... +func (s *Service) AccessTokenPOST(c echo.Context) error { req := c.Request() ctype := req.Header.Get("Content-Type") if ctype != "application/x-www-form-urlencoded" { - retErr := struct { - Err string `json:"error"` - Desc string `json:"error_description"` - URI string `json:"error_url"` - }{ - Err: "invalid request", - Desc: "Content-Type must be application/x-www-form-urlencoded", - URI: s.config.DocumentationURL, + return s.accessTokenError(c, "invalid_request", + "Content-Type must be application/x-www-form-urlencoded", 400) + } + + params, err := c.FormParams() + if err != nil { + return err + } + grantType := params.Get("grant_type") + code := params.Get("code") + redirectURI := params.Get("redirect_uri") + clientID := params.Get("client_id") + clientSecret := params.Get("client_secret") + + auth := req.Header.Get("Authorization") + if auth != "" && (clientID != "" || clientSecret != "") { + return s.accessTokenError(c, "invalid_client", + "Cannot supply both client_id & client_secret and Authorization header", 400) + } else if auth != "" { + parts := strings.SplitN(auth, " ", 2) + if len(parts) != 2 || parts[0] != "Basic" { + return s.accessTokenError(c, "invalid_client", + "Invalid Authorization header", 400) + } + bytes, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + return s.accessTokenError(c, "invalid_client", + "Invalid Authorization header contents", 400) + } + auth = string(bytes) + if !strings.Contains(auth, ":") { + return s.accessTokenError(c, "invalid_client", + "Invalid Authorization header", 400) + } + parts = strings.SplitN(auth, ":", 2) + clientID, err = url.PathUnescape(parts[0]) + if err != nil { + return s.accessTokenError(c, "invalid_client", + "Invalid Authorization header contents", 400) + } + clientSecret, err = url.PathUnescape(parts[1]) + if err != nil { + return s.accessTokenError(c, "invalid_client", + "Invalid Authorization header contents", 400) } - return c.JSON(http.StatusBadRequest, &retErr) + } else if clientID == "" || clientSecret == "" { + return s.accessTokenError(c, "invalid_client", + "Missing client authorization", 401) + } + + if grantType == "" { + return s.accessTokenError(c, "invalid_request", + "The grant_type parameter is required", 400) + } + if grantType != "authorization_code" { + return s.accessTokenError(c, "unsupported_grant_type", + fmt.Sprintf("Unsupported grant type %s", grantType), 400) + } + if code == "" { + return s.accessTokenError(c, "invalid_request", + "The code parameter is required", 400) + } + + opts := &database.FilterOptions{ + Filter: sq.Eq{"code": code}, + } + auths, err := GetAuthorizations(c.Request().Context(), opts) + if err != nil { + return s.accessTokenError(c, "server_error", + "server error occurred, try again.", 400) + } + if len(auths) == 0 { + return s.accessTokenError(c, "invalid_request", + "Invalid authorization code", 400) + } + authCode := auths[0] + if authCode.IsExpired() { + authCode.Delete(c.Request().Context()) + return s.accessTokenError(c, "invalid_request", + "Authorization code expired", 400) + } + + var payload AuthorizationPayload + if err = json.Unmarshal([]byte(authCode.Payload), &payload); err != nil { + panic(err) + } + + issued := time.Now().UTC() + expires := issued.Add(366 * 24 * time.Hour) + + client, err := GetClientByID(c.Request().Context(), clientID) + if err != nil { + return s.accessTokenError(c, "invalid_request", + "Invalid client id", 400) + } + if !client.VerifyClientSecret(clientSecret) { + return s.accessTokenError(c, "invalid_request", + "Invalid client secret", 400) + } + + if redirectURI != "" && redirectURI != client.RedirectURL { + return s.accessTokenError(c, "invalid_request", + "Invalid redirect URI", 400) + } + + bt := BearerToken{ + Version: TokenVersion, + Issued: ToTimestamp(issued), + Expires: ToTimestamp(expires), + Grants: payload.Grants, + UserID: payload.UserID, + ClientID: payload.ClientKey, + } + + token := bt.Encode(c.Request().Context()) + hash := sha512.Sum512([]byte(token)) + tokenHash := hex.EncodeToString(hash[:]) + + grant := &Grant{ + Issued: issued, + Expires: expires, + TokenHash: tokenHash, + UserID: payload.UserID, + ClientID: sql.NullInt64{Int64: int64(client.ID), Valid: true}, + } + if err := grant.Store(c.Request().Context()); err != nil { + return s.accessTokenError(c, "server_error", + "server error occurred storing token, try again.", 400) + } + + authCode.Delete(c.Request().Context()) + + ret := struct { + Token string `json:"access_token"` + Type string `json:"token_type"` + Expires int `json:"expires_in"` + Scope string `json:"scope"` + }{ + Token: token, + Type: "bearer", + Expires: int(expires.Sub(time.Now().UTC()).Seconds()), + Scope: payload.Grants, + } + + return c.JSON(http.StatusOK, &ret) +} + +// IntrospectPOST ... +func (s *Service) IntrospectPOST(c echo.Context) error { + req := c.Request() + ctype := req.Header.Get("Content-Type") + if ctype != "application/x-www-form-urlencoded" { + return s.accessTokenError(c, "invalid_request", + "Content-Type must be application/x-www-form-urlencoded", 400) } retFalse := struct { @@ -378,11 +542,11 @@ } // NewService return service -func NewService(eg *echo.Group, name string, helper Helper, config *ServiceConfig) *Service { +func NewService(eg *echo.Group, name string, config *ServiceConfig) *Service { if name == "" { name = "oauth2" } - service := &Service{name: name, eg: eg, helper: helper, config: config} + service := &Service{name: name, eg: eg, config: config} service.RegisterRoutes() return service }