@@ 3,6 3,8 @@ package oauth2
import (
"crypto/rand"
"crypto/sha512"
+ "database/sql"
+ "encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
@@ 21,6 23,7 @@ import (
// ServiceConfig let's you add basic config variables to service
type ServiceConfig struct {
+ Helper Helper
DocumentationURL string
Scopes []string
}
@@ 29,13 32,13 @@ type ServiceConfig struct {
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 @@ func (s *Service) AddClient(c echo.Context) error {
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 @@ func (s *Service) AuthorizePOST(c echo.Context) error {
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)
}
- return c.JSON(http.StatusBadRequest, &retErr)
+ 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)
+ }
+ } 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 @@ func (s *Service) RouteName(value string) string {
}
// 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
}