♻️ Refactored auth package
This commit is contained in:
parent
60da2ac25e
commit
83dc4286eb
|
@ -1,348 +1,359 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"fmt"
|
||||||
"time"
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/fasthttp/router"
|
"github.com/fasthttp/router"
|
||||||
json "github.com/goccy/go-json"
|
json "github.com/goccy/go-json"
|
||||||
http "github.com/valyala/fasthttp"
|
http "github.com/valyala/fasthttp"
|
||||||
"gitlab.com/toby3d/indieauth/internal/auth"
|
"golang.org/x/text/language"
|
||||||
"gitlab.com/toby3d/indieauth/internal/domain"
|
"golang.org/x/text/message"
|
||||||
"gitlab.com/toby3d/indieauth/internal/middleware"
|
"golang.org/x/xerrors"
|
||||||
"gitlab.com/toby3d/indieauth/internal/pkce"
|
|
||||||
"gitlab.com/toby3d/indieauth/web"
|
"source.toby3d.me/toby3d/form"
|
||||||
|
"source.toby3d.me/toby3d/middleware"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/auth"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/client"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/common"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/indieauth/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Handler struct {
|
|
||||||
useCase auth.UseCase
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthorizeRequest struct {
|
AuthorizeRequest struct {
|
||||||
RedirectURI string
|
// Indicates to the authorization server that an authorization
|
||||||
ResponseType string
|
// code should be returned as the response.
|
||||||
ClientID string
|
ResponseType domain.ResponseType `form:"response_type"` // code
|
||||||
State []byte
|
|
||||||
Scope string
|
// The client URL.
|
||||||
CodeChallenge string
|
ClientID *domain.ClientID `form:"client_id"`
|
||||||
CodeChallengeMethod string
|
|
||||||
Me string
|
// The redirect URL indicating where the user should be
|
||||||
|
// redirected to after approving the request.
|
||||||
|
RedirectURI *domain.URL `form:"redirect_uri"`
|
||||||
|
|
||||||
|
// A parameter set by the client which will be included when the
|
||||||
|
// user is redirected back to the client. This is used to
|
||||||
|
// prevent CSRF attacks. The authorization server MUST return
|
||||||
|
// the unmodified state value back to the client.
|
||||||
|
State string `form:"state"`
|
||||||
|
|
||||||
|
// The code challenge as previously described.
|
||||||
|
CodeChallenge string `form:"code_challenge"`
|
||||||
|
|
||||||
|
// The hashing method used to calculate the code challenge.
|
||||||
|
CodeChallengeMethod domain.CodeChallengeMethod `form:"code_challenge_method"`
|
||||||
|
|
||||||
|
// A space-separated list of scopes the client is requesting,
|
||||||
|
// e.g. "profile", or "profile create". If the client omits this
|
||||||
|
// value, the authorization server MUST NOT issue an access
|
||||||
|
// token for this authorization code. Only the user's profile
|
||||||
|
// URL may be returned without any scope requested.
|
||||||
|
Scope domain.Scopes `form:"scope"`
|
||||||
|
|
||||||
|
// The URL that the user entered.
|
||||||
|
Me *domain.Me `form:"me"`
|
||||||
}
|
}
|
||||||
|
|
||||||
RedirectRequest struct {
|
VerifyRequest struct {
|
||||||
Authorize string
|
ClientID *domain.ClientID `form:"client_id"`
|
||||||
ClientID string
|
Me *domain.Me `form:"me"`
|
||||||
CodeChallenge string
|
RedirectURI *domain.URL `form:"redirect_uri"`
|
||||||
CodeChallengeMethod string
|
CodeChallengeMethod domain.CodeChallengeMethod `form:"code_challenge_method"`
|
||||||
Me string
|
ResponseType domain.ResponseType `form:"response_type"`
|
||||||
RedirectURI string
|
Scope domain.Scopes `form:"scope[]"` // TODO(toby3d): fix parsing in form pkg
|
||||||
ResponseType string
|
Authorize string `form:"authorize"`
|
||||||
Scope string
|
CodeChallenge string `form:"code_challenge"`
|
||||||
State []byte
|
State string `form:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
ExchangeRequest struct {
|
ExchangeRequest struct {
|
||||||
GrantType string
|
GrantType domain.GrantType `form:"grant_type"` // authorization_code
|
||||||
Code string
|
|
||||||
ClientID string
|
// The authorization code received from the authorization
|
||||||
RedirectURI string
|
// endpoint in the redirect.
|
||||||
CodeVerifier string
|
Code string `form:"code"`
|
||||||
|
|
||||||
|
// The client's URL, which MUST match the client_id used in the
|
||||||
|
// authentication request.
|
||||||
|
ClientID *domain.ClientID `form:"client_id"`
|
||||||
|
|
||||||
|
// The client's redirect URL, which MUST match the initial
|
||||||
|
// authentication request.
|
||||||
|
RedirectURI *domain.URL `form:"redirect_uri"`
|
||||||
|
|
||||||
|
// The original plaintext random string generated before
|
||||||
|
// starting the authorization request.
|
||||||
|
CodeVerifier string `form:"code_verifier"`
|
||||||
}
|
}
|
||||||
|
|
||||||
ExchangeResponse struct {
|
ExchangeResponse struct {
|
||||||
Me string `json:"me"`
|
Me *domain.Me `json:"me"`
|
||||||
|
}
|
||||||
|
|
||||||
|
NewRequestHandlerOptions struct {
|
||||||
|
Auth auth.UseCase
|
||||||
|
Clients client.UseCase
|
||||||
|
Config *domain.Config
|
||||||
|
Matcher language.Matcher
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestHandler struct {
|
||||||
|
clients client.UseCase
|
||||||
|
config *domain.Config
|
||||||
|
matcher language.Matcher
|
||||||
|
useCase auth.UseCase
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAuthHandler(useCase auth.UseCase) *Handler {
|
func NewRequestHandler(opts NewRequestHandlerOptions) *RequestHandler {
|
||||||
return &Handler{
|
return &RequestHandler{
|
||||||
useCase: useCase,
|
clients: opts.Clients,
|
||||||
|
config: opts.Config,
|
||||||
|
matcher: opts.Matcher,
|
||||||
|
useCase: opts.Auth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Register(r *router.Router) {
|
func (h *RequestHandler) Register(r *router.Router) {
|
||||||
chain := middleware.Chain{middleware.CSRFWithConfig(middleware.CSRFConfig{
|
chain := middleware.Chain{
|
||||||
ContextKey: "csrf",
|
middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||||
CookieHTTPOnly: true,
|
Skipper: func(ctx *http.RequestCtx) bool {
|
||||||
CookieName: "__Host-CSRF",
|
matched, _ := path.Match("/api/*", string(ctx.Path()))
|
||||||
CookiePath: "/",
|
|
||||||
CookieSameSite: http.CookieSameSiteLaxMode,
|
|
||||||
CookieSecure: true,
|
|
||||||
TokenLookup: "form:_csrf",
|
|
||||||
Skipper: func(ctx *http.RequestCtx) bool {
|
|
||||||
return ctx.IsPost() && ctx.PostArgs().Has("grant_type") &&
|
|
||||||
string(ctx.PostArgs().Peek("grant_type")) == "authorization_code"
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
|
|
||||||
r.GET("/authorize", chain.RequestHandler(h.ClientInfo))
|
return ctx.IsPost() && matched
|
||||||
r.POST("/authorize", chain.RequestHandler(h.Update))
|
},
|
||||||
|
CookieSameSite: http.CookieSameSiteLaxMode,
|
||||||
|
CookieName: "_csrf",
|
||||||
|
TokenLookup: "form:_csrf",
|
||||||
|
CookieSecure: true,
|
||||||
|
CookieHTTPOnly: true,
|
||||||
|
}),
|
||||||
|
middleware.LogFmt(),
|
||||||
|
}
|
||||||
|
|
||||||
|
r.GET("/authorize", chain.RequestHandler(h.handleRender))
|
||||||
|
r.POST("/api/authorize", chain.RequestHandler(h.handleVerify))
|
||||||
|
r.POST("/authorize", chain.RequestHandler(h.handleExchange))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AuthorizeRequest) bind(ctx *http.RequestCtx) error {
|
func (h *RequestHandler) handleRender(ctx *http.RequestCtx) {
|
||||||
if r.ClientID = string(ctx.QueryArgs().Peek("client_id")); r.ClientID == "" {
|
req := new(AuthorizeRequest)
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "'client_id' query is required",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.ResponseType = string(ctx.QueryArgs().Peek("response_type")); r.ResponseType != "code" {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "'response_type' must be 'code'",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.QueryArgs().Has("code_challenge") {
|
|
||||||
r.CodeChallenge = string(ctx.QueryArgs().Peek("code_challenge"))
|
|
||||||
if len(r.CodeChallenge) < 43 || len(r.CodeChallenge) > 128 {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "length of the 'code_challenge' value must be greater than 43 and less than 128 symbols",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.CodeChallengeMethod = pkce.DefaultMethod
|
|
||||||
if ctx.PostArgs().Has("code_challenge_method") {
|
|
||||||
r.CodeChallengeMethod = string(ctx.QueryArgs().Peek("code_challenge_method"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := pkce.New(r.CodeChallengeMethod); err != nil {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: err.Error(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.RedirectURI = string(ctx.QueryArgs().Peek("redirect_uri"))
|
|
||||||
r.State = ctx.QueryArgs().Peek("state")
|
|
||||||
r.Scope = string(ctx.QueryArgs().Peek("scope"))
|
|
||||||
r.Me = string(ctx.QueryArgs().Peek("me"))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) ClientInfo(ctx *http.RequestCtx) {
|
|
||||||
r := new(AuthorizeRequest)
|
|
||||||
r.Scope = "profile"
|
|
||||||
|
|
||||||
if err := r.bind(ctx); err != nil {
|
|
||||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := h.useCase.Discovery(ctx, r.ClientID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
csrf, _ := ctx.UserValue("csrf").([]byte)
|
|
||||||
|
|
||||||
ctx.SetContentType("text/html")
|
|
||||||
web.WritePageTemplate(ctx, &web.AuthPage{
|
|
||||||
Client: client,
|
|
||||||
CodeChallenge: r.CodeChallenge,
|
|
||||||
CodeChallengeMethod: r.CodeChallengeMethod,
|
|
||||||
CSRF: csrf,
|
|
||||||
Me: r.Me,
|
|
||||||
RedirectURI: r.RedirectURI,
|
|
||||||
ResponseType: r.ResponseType,
|
|
||||||
Scope: r.Scope,
|
|
||||||
State: r.State,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) Update(ctx *http.RequestCtx) {
|
|
||||||
if ctx.PostArgs().Has("response_type") && string(ctx.PostArgs().Peek("response_type")) == "code" {
|
|
||||||
h.Redirect(ctx)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.PostArgs().Has("grant_type") && string(ctx.PostArgs().Peek("grant_type")) == "authorization_code" {
|
|
||||||
h.Exchange(ctx)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Error("please, restart your authoriztion flow", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RedirectRequest) bind(ctx *http.RequestCtx) (err error) {
|
|
||||||
r.RedirectURI = string(ctx.PostArgs().Peek("redirect_uri"))
|
|
||||||
|
|
||||||
if r.ClientID = string(ctx.PostArgs().Peek("client_id")); r.ClientID == "" {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "'client_id' query is required",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Authorize = string(ctx.PostArgs().Peek("authorize")); r.Authorize != "allow" && r.Authorize != "deny" {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "invalid prompt action, try starting the authorization flow again",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.ResponseType = string(ctx.PostArgs().Peek("response_type")); r.ResponseType != "code" {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "'response_type' must be 'code', try starting the authorization flow again",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.PostArgs().Has("code_challenge") {
|
|
||||||
r.CodeChallenge = string(ctx.PostArgs().Peek("code_challenge"))
|
|
||||||
|
|
||||||
if len(r.CodeChallenge) < 43 || len(r.CodeChallenge) > 128 {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "length of the 'code_challenge' value must be greater than 43 and less than 128 symbols, try starting the authorization flow again",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.CodeChallengeMethod = pkce.DefaultMethod
|
|
||||||
if ctx.PostArgs().Has("code_challenge_method") {
|
|
||||||
r.CodeChallengeMethod = string(ctx.PostArgs().Peek("code_challenge_method"))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := pkce.New(r.CodeChallengeMethod)
|
|
||||||
if err != nil {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: err.Error(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.State = ctx.PostArgs().Peek("state")
|
|
||||||
r.Scope = string(ctx.PostArgs().Peek("scope"))
|
|
||||||
r.Me = string(ctx.PostArgs().Peek("me"))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) Redirect(ctx *http.RequestCtx) {
|
|
||||||
r := new(RedirectRequest)
|
|
||||||
if err := r.bind(ctx); err != nil {
|
|
||||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectUri, err := url.Parse(r.RedirectURI)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := redirectUri.Query()
|
|
||||||
query.Set("state", string(r.State))
|
|
||||||
|
|
||||||
switch r.Authorize {
|
|
||||||
case "allow":
|
|
||||||
code, err := h.useCase.Approve(ctx, &domain.Login{
|
|
||||||
CreatedAt: time.Now().UTC().Unix(),
|
|
||||||
ClientID: r.ClientID,
|
|
||||||
CodeChallenge: r.CodeChallenge,
|
|
||||||
CodeChallengeMethod: r.CodeChallengeMethod,
|
|
||||||
Me: r.Me,
|
|
||||||
RedirectURI: r.RedirectURI,
|
|
||||||
Scope: r.Scope,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
query.Set("error", domain.ErrServerError.Code)
|
|
||||||
query.Set("error_description", err.Error())
|
|
||||||
|
|
||||||
redirectUri.RawQuery = query.Encode()
|
|
||||||
|
|
||||||
ctx.Redirect(redirectUri.String(), http.StatusFound)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query.Set("code", code)
|
|
||||||
case "deny":
|
|
||||||
query.Set("error", domain.ErrAccessDenied.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectUri.RawQuery = query.Encode()
|
|
||||||
|
|
||||||
ctx.Redirect(redirectUri.String(), http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ExchangeRequest) bind(ctx *http.RequestCtx) (err error) {
|
|
||||||
if r.GrantType = string(ctx.PostArgs().Peek("grant_type")); r.GrantType != "authorization_code" {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "'grant_type' must be 'authorization_code'",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.RedirectURI = string(ctx.PostArgs().Peek("redirect_uri")); r.RedirectURI == "" {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "'redirect_uri' query is required",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.ClientID = string(ctx.PostArgs().Peek("client_id")); r.ClientID == "" {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "'client_id' query is required",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Code = string(ctx.PostArgs().Peek("code")); r.Code == "" {
|
|
||||||
return domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "'code' query is required",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.CodeVerifier = string(ctx.PostArgs().Peek("code_verifier"))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) Exchange(ctx *http.RequestCtx) {
|
|
||||||
encoder := json.NewEncoder(ctx)
|
|
||||||
req := new(ExchangeRequest)
|
|
||||||
|
|
||||||
if err := req.bind(ctx); err != nil {
|
if err := req.bind(ctx); err != nil {
|
||||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
ctx.Error(err.Error(), http.StatusBadRequest)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
me, err := h.useCase.Exchange(ctx, &domain.ExchangeRequest{
|
client, err := h.clients.Discovery(ctx, req.ClientID)
|
||||||
ClientID: req.ClientID,
|
|
||||||
Code: req.Code,
|
|
||||||
CodeVerifier: req.CodeVerifier,
|
|
||||||
RedirectURI: req.RedirectURI,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
ctx.Error(err.Error(), http.StatusBadRequest)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if me == "" {
|
if !client.ValidateRedirectURI(req.RedirectURI) {
|
||||||
ctx.Error(domain.ErrUnauthorizedClient.Error(), http.StatusUnauthorized)
|
ctx.Error("requested redirect_uri is not registered on client_id side", http.StatusBadRequest)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.SetContentType("application/json")
|
csrf, _ := ctx.UserValue(middleware.DefaultCSRFConfig.ContextKey).([]byte)
|
||||||
_ = encoder.Encode(&ExchangeResponse{
|
tags, _, _ := language.ParseAcceptLanguage(string(ctx.Request.Header.Peek(http.HeaderAcceptLanguage)))
|
||||||
|
tag, _, _ := h.matcher.Match(tags...)
|
||||||
|
|
||||||
|
ctx.SetContentType(common.MIMETextHTMLCharsetUTF8)
|
||||||
|
web.WriteTemplate(ctx, &web.AuthorizePage{
|
||||||
|
BaseOf: web.BaseOf{
|
||||||
|
Config: h.config,
|
||||||
|
Language: tag,
|
||||||
|
Printer: message.NewPrinter(tag),
|
||||||
|
},
|
||||||
|
Client: client,
|
||||||
|
CodeChallenge: req.CodeChallenge,
|
||||||
|
CodeChallengeMethod: req.CodeChallengeMethod,
|
||||||
|
CSRF: csrf,
|
||||||
|
Me: req.Me,
|
||||||
|
RedirectURI: req.RedirectURI,
|
||||||
|
ResponseType: req.ResponseType,
|
||||||
|
Scope: req.Scope,
|
||||||
|
State: req.State,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RequestHandler) handleVerify(ctx *http.RequestCtx) {
|
||||||
|
ctx.SetContentType(common.MIMEApplicationJSONCharsetUTF8)
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(ctx)
|
||||||
|
|
||||||
|
req := new(VerifyRequest)
|
||||||
|
if err := req.bind(ctx); err != nil {
|
||||||
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
|
encoder.Encode(domain.Error{
|
||||||
|
Code: "invalid_request",
|
||||||
|
Description: err.Error(),
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u := http.AcquireURI()
|
||||||
|
defer http.ReleaseURI(u)
|
||||||
|
req.RedirectURI.CopyTo(u)
|
||||||
|
|
||||||
|
if strings.EqualFold(req.Authorize, "deny") {
|
||||||
|
u.QueryArgs().Set("error", "access_denied")
|
||||||
|
u.QueryArgs().Set("error_description", "user deny authorization request")
|
||||||
|
ctx.Redirect(u.String(), http.StatusFound)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := h.useCase.Generate(ctx, auth.GenerateOptions{
|
||||||
|
ClientID: req.ClientID,
|
||||||
|
RedirectURI: req.RedirectURI,
|
||||||
|
CodeChallenge: req.CodeChallenge,
|
||||||
|
CodeChallengeMethod: req.CodeChallengeMethod,
|
||||||
|
Scope: req.Scope,
|
||||||
|
Me: req.Me,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.SetStatusCode(http.StatusInternalServerError)
|
||||||
|
encoder.Encode(domain.Error{
|
||||||
|
Description: err.Error(),
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val := range map[string]string{
|
||||||
|
"code": code,
|
||||||
|
"iss": h.config.Server.GetRootURL(),
|
||||||
|
"state": req.State,
|
||||||
|
} {
|
||||||
|
u.QueryArgs().Set(key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Redirect(u.String(), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RequestHandler) handleExchange(ctx *http.RequestCtx) {
|
||||||
|
ctx.SetContentType(common.MIMEApplicationJSONCharsetUTF8)
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(ctx)
|
||||||
|
|
||||||
|
req := new(ExchangeRequest)
|
||||||
|
if err := req.bind(ctx); err != nil {
|
||||||
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
|
encoder.Encode(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
me, err := h.useCase.Exchange(ctx, auth.ExchangeOptions{
|
||||||
|
Code: req.Code,
|
||||||
|
ClientID: req.ClientID,
|
||||||
|
RedirectURI: req.RedirectURI,
|
||||||
|
CodeVerifier: req.CodeVerifier,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
|
encoder.Encode(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder.Encode(&ExchangeResponse{
|
||||||
Me: me,
|
Me: me,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *AuthorizeRequest) bind(ctx *http.RequestCtx) error {
|
||||||
|
if err := form.Unmarshal(ctx.QueryArgs(), r); err != nil {
|
||||||
|
return domain.Error{
|
||||||
|
Code: "invalid_request",
|
||||||
|
Description: err.Error(),
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Scope = make(domain.Scopes, 0)
|
||||||
|
parseScope(r.Scope, ctx.QueryArgs().Peek("scope"))
|
||||||
|
|
||||||
|
if r.ResponseType == domain.ResponseTypeID {
|
||||||
|
r.ResponseType = domain.ResponseTypeCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VerifyRequest) bind(ctx *http.RequestCtx) error {
|
||||||
|
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
|
||||||
|
return domain.Error{
|
||||||
|
Code: "invalid_request",
|
||||||
|
Description: err.Error(),
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Scope = make(domain.Scopes, 0)
|
||||||
|
parseScope(r.Scope, ctx.PostArgs().PeekMulti("scope[]")...)
|
||||||
|
|
||||||
|
if r.ResponseType == domain.ResponseTypeID {
|
||||||
|
r.ResponseType = domain.ResponseTypeCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(r.Authorize, "allow") && !strings.EqualFold(r.Authorize, "deny") {
|
||||||
|
return domain.Error{
|
||||||
|
Code: "invalid_request",
|
||||||
|
Description: "cannot validate verification request",
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ExchangeRequest) bind(ctx *http.RequestCtx) error {
|
||||||
|
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
|
||||||
|
return domain.Error{
|
||||||
|
Code: "invalid_request",
|
||||||
|
Description: err.Error(),
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(toby3d): fix this in form pkg.
|
||||||
|
func parseScope(dst domain.Scopes, src ...[]byte) error {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopes []string
|
||||||
|
|
||||||
|
if len(src) == 1 {
|
||||||
|
scopes = strings.Fields(string(src[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rawScope := range scopes {
|
||||||
|
scope, err := domain.ParseScope(string(rawScope))
|
||||||
|
if err != nil {
|
||||||
|
return &domain.Error{
|
||||||
|
Code: "invalid_request",
|
||||||
|
Description: fmt.Sprintf("cannot parse scope: %v", err),
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dst = append(dst, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package http_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fasthttp/router"
|
||||||
|
"github.com/fasthttp/session/v2"
|
||||||
|
"github.com/fasthttp/session/v2/providers/memory"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
http "github.com/valyala/fasthttp"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"golang.org/x/text/message"
|
||||||
|
|
||||||
|
delivery "source.toby3d.me/website/indieauth/internal/auth/delivery/http"
|
||||||
|
ucase "source.toby3d.me/website/indieauth/internal/auth/usecase"
|
||||||
|
clientrepo "source.toby3d.me/website/indieauth/internal/client/repository/memory"
|
||||||
|
clientucase "source.toby3d.me/website/indieauth/internal/client/usecase"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
|
sessionrepo "source.toby3d.me/website/indieauth/internal/session/repository/memory"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/testing/httptest"
|
||||||
|
userrepo "source.toby3d.me/website/indieauth/internal/user/repository/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRender(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
provider, err := memory.New(memory.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s := session.New(session.NewDefaultConfig())
|
||||||
|
require.NoError(t, s.SetProvider(provider))
|
||||||
|
|
||||||
|
me := domain.TestMe(t)
|
||||||
|
c := domain.TestClient(t)
|
||||||
|
config := domain.TestConfig(t)
|
||||||
|
store := new(sync.Map)
|
||||||
|
store.Store(path.Join(userrepo.DefaultPathPrefix, me.String()), domain.TestUser(t))
|
||||||
|
store.Store(path.Join(clientrepo.DefaultPathPrefix, c.ID.String()), c)
|
||||||
|
|
||||||
|
r := router.New()
|
||||||
|
delivery.NewRequestHandler(delivery.NewRequestHandlerOptions{
|
||||||
|
Clients: clientucase.NewClientUseCase(clientrepo.NewMemoryClientRepository(store)),
|
||||||
|
Config: config,
|
||||||
|
Matcher: language.NewMatcher(message.DefaultCatalog.Languages()),
|
||||||
|
Auth: ucase.NewAuthUseCase(sessionrepo.NewMemorySessionRepository(config, store), config),
|
||||||
|
}).Register(r)
|
||||||
|
|
||||||
|
client, _, cleanup := httptest.New(t, r.Handler)
|
||||||
|
t.Cleanup(cleanup)
|
||||||
|
|
||||||
|
u := http.AcquireURI()
|
||||||
|
defer http.ReleaseURI(u)
|
||||||
|
u.Update("https://example.com/authorize")
|
||||||
|
|
||||||
|
for k, v := range map[string]string{
|
||||||
|
"client_id": c.ID.String(),
|
||||||
|
"code_challenge": "OfYAxt8zU2dAPDWQxTAUIteRzMsoj9QBdMIVEDOErUo",
|
||||||
|
"code_challenge_method": domain.CodeChallengeMethodS256.String(),
|
||||||
|
"me": me.String(),
|
||||||
|
"redirect_uri": c.RedirectURI[0].String(),
|
||||||
|
"response_type": domain.ResponseTypeCode.String(),
|
||||||
|
"scope": "profile email",
|
||||||
|
"state": "1234567890",
|
||||||
|
} {
|
||||||
|
u.QueryArgs().Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, u.String(), nil)
|
||||||
|
defer http.ReleaseRequest(req)
|
||||||
|
|
||||||
|
resp := http.AcquireResponse()
|
||||||
|
defer http.ReleaseResponse(resp)
|
||||||
|
|
||||||
|
require.NoError(t, client.Do(req, resp))
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode())
|
||||||
|
assert.Contains(t, string(resp.Body()), `Authorize application`)
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Repository interface {
|
|
||||||
Create(ctx context.Context, login *domain.Login) error
|
|
||||||
Get(ctx context.Context, code string) (*domain.Login, error)
|
|
||||||
Delete(ctx context.Context, code string) error
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package bolt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
json "github.com/goccy/go-json"
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/auth"
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/domain"
|
|
||||||
bolt "go.etcd.io/bbolt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type boltAuthRepository struct {
|
|
||||||
db *bolt.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBoltAuthRepository(db *bolt.DB) (auth.Repository, error) {
|
|
||||||
if err := db.Update(func(tx *bolt.Tx) (err error) {
|
|
||||||
_, err = tx.CreateBucketIfNotExists(domain.Login{}.Bucket())
|
|
||||||
|
|
||||||
return err
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &boltAuthRepository{
|
|
||||||
db: db,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *boltAuthRepository) Create(ctx context.Context, login *domain.Login) error {
|
|
||||||
jsonLogin, err := json.Marshal(login)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo.db.Update(func(tx *bolt.Tx) error {
|
|
||||||
return tx.Bucket(domain.Login{}.Bucket()).Put([]byte(login.Code), jsonLogin)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *boltAuthRepository) Get(ctx context.Context, code string) (*domain.Login, error) {
|
|
||||||
login := new(domain.Login)
|
|
||||||
|
|
||||||
if err := repo.db.View(func(tx *bolt.Tx) error {
|
|
||||||
return json.Unmarshal(tx.Bucket(domain.Login{}.Bucket()).Get([]byte(code)), login)
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return login, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *boltAuthRepository) Delete(ctx context.Context, code string) error {
|
|
||||||
return repo.db.Update(func(tx *bolt.Tx) error {
|
|
||||||
return tx.Bucket(domain.Login{}.Bucket()).Delete([]byte(code))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/auth"
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
type memoryAuthRepository struct {
|
|
||||||
logins *sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMemoryAuthRepository() auth.Repository {
|
|
||||||
return &memoryAuthRepository{
|
|
||||||
logins: new(sync.Map),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *memoryAuthRepository) Create(ctx context.Context, login *domain.Login) error {
|
|
||||||
repo.logins.Store(login.Code, login)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *memoryAuthRepository) Get(ctx context.Context, code string) (*domain.Login, error) {
|
|
||||||
login, ok := repo.logins.LoadAndDelete(code)
|
|
||||||
if !ok {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return login.(*domain.Login), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *memoryAuthRepository) Delete(ctx context.Context, code string) error {
|
|
||||||
repo.logins.Delete(code)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"source.toby3d.me/website/indieauth/internal/auth"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/random"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authUseCase struct {
|
||||||
|
config *domain.Config
|
||||||
|
sessions session.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthUseCase(sessions session.Repository, config *domain.Config) auth.UseCase {
|
||||||
|
return &authUseCase{
|
||||||
|
config: config,
|
||||||
|
sessions: sessions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (useCase *authUseCase) Generate(ctx context.Context, opts auth.GenerateOptions) (string, error) {
|
||||||
|
code, err := random.String(useCase.config.Code.Length)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot generate random code: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = useCase.sessions.Create(ctx, &domain.Session{
|
||||||
|
ClientID: opts.ClientID,
|
||||||
|
Code: code,
|
||||||
|
CodeChallenge: opts.CodeChallenge,
|
||||||
|
CodeChallengeMethod: opts.CodeChallengeMethod,
|
||||||
|
Me: opts.Me,
|
||||||
|
RedirectURI: opts.RedirectURI,
|
||||||
|
Scope: opts.Scope,
|
||||||
|
}); err != nil {
|
||||||
|
return "", fmt.Errorf("cannot save session in store: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (useCase *authUseCase) Exchange(ctx context.Context, opts auth.ExchangeOptions) (*domain.Me, error) {
|
||||||
|
session, err := useCase.sessions.GetAndDelete(ctx, opts.Code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.ClientID.String() != session.ClientID.String() {
|
||||||
|
return nil, domain.Error{
|
||||||
|
Code: "invalid_request",
|
||||||
|
Description: "client's URL MUST match the client_id used in the authentication request",
|
||||||
|
URI: "https://indieauth.net/source/#request",
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.RedirectURI.String() != session.RedirectURI.String() {
|
||||||
|
return nil, domain.Error{
|
||||||
|
Code: "invalid_request",
|
||||||
|
Description: "client's redirect URL MUST match the initial authentication request",
|
||||||
|
URI: "https://indieauth.net/source/#request",
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.CodeChallenge != "" &&
|
||||||
|
!session.CodeChallengeMethod.Validate(session.CodeChallenge, opts.CodeVerifier) {
|
||||||
|
return nil, domain.Error{
|
||||||
|
Code: "invalid_request",
|
||||||
|
Description: "code_verifier is not hashes to the same value as given in " +
|
||||||
|
"the code_challenge in the original authorization request",
|
||||||
|
URI: "https://indieauth.net/source/#request",
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.Me, nil
|
||||||
|
}
|
|
@ -1,137 +0,0 @@
|
||||||
package usecase
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
http "github.com/valyala/fasthttp"
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/auth"
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/domain"
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/pkce"
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/random"
|
|
||||||
"willnorris.com/go/microformats"
|
|
||||||
)
|
|
||||||
|
|
||||||
type authUseCase struct {
|
|
||||||
client *http.Client
|
|
||||||
repo auth.Repository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAuthUseCase(repo auth.Repository) auth.UseCase {
|
|
||||||
return &authUseCase{
|
|
||||||
client: new(http.Client),
|
|
||||||
repo: repo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (useCase *authUseCase) Discovery(ctx context.Context, clientId string) (*domain.Client, error) {
|
|
||||||
_, src, err := useCase.client.Get(nil, clientId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cid, err := url.Parse(clientId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data := microformats.Parse(bytes.NewReader(src), cid)
|
|
||||||
|
|
||||||
client := new(domain.Client)
|
|
||||||
client.RedirectURI = make([]string, 0)
|
|
||||||
|
|
||||||
for i := range data.Items {
|
|
||||||
if len(data.Items[i].Type) == 0 || data.Items[i].Type[0] != "h-app" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, values := range data.Items[i].Properties {
|
|
||||||
switch key {
|
|
||||||
case "logo":
|
|
||||||
for j := range values {
|
|
||||||
switch val := values[j].(type) {
|
|
||||||
case string:
|
|
||||||
client.Logo = val
|
|
||||||
case map[string]string:
|
|
||||||
client.Logo = val["value"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "name":
|
|
||||||
for j := range values {
|
|
||||||
client.Name, _ = values[j].(string)
|
|
||||||
}
|
|
||||||
case "url":
|
|
||||||
for j := range values {
|
|
||||||
client.URL, _ = values[j].(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, values := range data.Rels {
|
|
||||||
if key != "redirect_uri" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
client.RedirectURI = append(client.RedirectURI, values...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if client.URL != clientId {
|
|
||||||
return nil, domain.Error{
|
|
||||||
Code: domain.ErrInvalidRequest.Code,
|
|
||||||
Description: "'client_id' does not match the actual client URL",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (useCase *authUseCase) Approve(ctx context.Context, login *domain.Login) (string, error) {
|
|
||||||
login.Code = random.New().String(32)
|
|
||||||
|
|
||||||
if err := useCase.repo.Create(ctx, login); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return login.Code, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (useCase *authUseCase) Exchange(ctx context.Context, req *domain.ExchangeRequest) (string, error) {
|
|
||||||
login, err := useCase.repo.Get(ctx, req.Code)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if login == nil {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = useCase.repo.Delete(ctx, req.Code)
|
|
||||||
|
|
||||||
if time.Now().UTC().After(time.Unix(login.CreatedAt, 0).Add(10 * time.Minute)) {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if login.ClientID != req.ClientID || login.RedirectURI != req.RedirectURI {
|
|
||||||
return "", domain.ErrInvalidRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
if login.CodeChallenge != "" {
|
|
||||||
codeChallenge, err := pkce.New(login.CodeChallengeMethod)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
codeChallenge.Verifier = req.CodeVerifier
|
|
||||||
|
|
||||||
codeChallenge.Generate()
|
|
||||||
|
|
||||||
if login.CodeChallenge != codeChallenge.Challenge {
|
|
||||||
return "", domain.ErrInvalidRequest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return login.Me, nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue