♻️ Simplify error usage in token package
This commit is contained in:
parent
ed55c8cded
commit
2e30613089
|
@ -1,17 +1,18 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"strings"
|
"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"
|
||||||
"golang.org/x/xerrors"
|
|
||||||
|
|
||||||
"source.toby3d.me/toby3d/form"
|
"source.toby3d.me/toby3d/form"
|
||||||
"source.toby3d.me/toby3d/middleware"
|
"source.toby3d.me/toby3d/middleware"
|
||||||
"source.toby3d.me/website/indieauth/internal/common"
|
"source.toby3d.me/website/indieauth/internal/common"
|
||||||
"source.toby3d.me/website/indieauth/internal/domain"
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/ticket"
|
||||||
"source.toby3d.me/website/indieauth/internal/token"
|
"source.toby3d.me/website/indieauth/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,10 +37,18 @@ type (
|
||||||
|
|
||||||
//nolint: tagliatelle
|
//nolint: tagliatelle
|
||||||
ExchangeResponse struct {
|
ExchangeResponse struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
Scope string `json:"scope"`
|
Scope string `json:"scope"`
|
||||||
Me string `json:"me"`
|
Me string `json:"me"`
|
||||||
|
Profile *ProfileResponse `json:"profile,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileResponse struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
URL *domain.URL `json:"url,omitempty"`
|
||||||
|
Photo *domain.URL `json:"photo,omitempty"`
|
||||||
|
Email *domain.Email `json:"email,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint: tagliatelle
|
//nolint: tagliatelle
|
||||||
|
@ -52,15 +61,15 @@ type (
|
||||||
RevocationResponse struct{}
|
RevocationResponse struct{}
|
||||||
|
|
||||||
RequestHandler struct {
|
RequestHandler struct {
|
||||||
tokens token.UseCase
|
tokens token.UseCase
|
||||||
// TODO(toby3d): tickets ticket.UseCase
|
tickets ticket.UseCase
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRequestHandler(tokens token.UseCase /*, tickets ticket.UseCase*/) *RequestHandler {
|
func NewRequestHandler(tokens token.UseCase, tickets ticket.UseCase) *RequestHandler {
|
||||||
return &RequestHandler{
|
return &RequestHandler{
|
||||||
tokens: tokens,
|
tokens: tokens,
|
||||||
// tickets: tickets,
|
tickets: tickets,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,16 +92,16 @@ func (h *RequestHandler) handleValidate(ctx *http.RequestCtx) {
|
||||||
"Bearer "))
|
"Bearer "))
|
||||||
if err != nil || t == nil {
|
if err != nil || t == nil {
|
||||||
ctx.SetStatusCode(http.StatusUnauthorized)
|
ctx.SetStatusCode(http.StatusUnauthorized)
|
||||||
encoder.Encode(&domain.Error{
|
encoder.Encode(domain.NewError(
|
||||||
Code: "unauthorized_client",
|
domain.ErrorCodeUnauthorizedClient,
|
||||||
Description: err.Error(),
|
err.Error(),
|
||||||
Frame: xerrors.Caller(1),
|
"https://indieauth.net/source/#access-token-verification",
|
||||||
})
|
))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder.Encode(&VerificationResponse{
|
_ = encoder.Encode(&VerificationResponse{
|
||||||
ClientID: t.ClientID,
|
ClientID: t.ClientID,
|
||||||
Me: t.Me,
|
Me: t.Me,
|
||||||
Scope: t.Scope,
|
Scope: t.Scope,
|
||||||
|
@ -111,11 +120,11 @@ func (h *RequestHandler) handleAction(ctx *http.RequestCtx) {
|
||||||
action, err := domain.ParseAction(string(ctx.PostArgs().Peek("action")))
|
action, err := domain.ParseAction(string(ctx.PostArgs().Peek("action")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SetStatusCode(http.StatusBadRequest)
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
encoder.Encode(domain.Error{
|
encoder.Encode(domain.NewError(
|
||||||
Code: "invalid_request",
|
domain.ErrorCodeInvalidRequest,
|
||||||
Description: err.Error(),
|
err.Error(),
|
||||||
Frame: xerrors.Caller(1),
|
"",
|
||||||
})
|
))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -142,7 +151,7 @@ func (h *RequestHandler) handleExchange(ctx *http.RequestCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := h.tokens.Exchange(ctx, token.ExchangeOptions{
|
token, profile, err := h.tokens.Exchange(ctx, token.ExchangeOptions{
|
||||||
ClientID: req.ClientID,
|
ClientID: req.ClientID,
|
||||||
RedirectURI: req.RedirectURI,
|
RedirectURI: req.RedirectURI,
|
||||||
Code: req.Code,
|
Code: req.Code,
|
||||||
|
@ -150,20 +159,47 @@ func (h *RequestHandler) handleExchange(ctx *http.RequestCtx) {
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SetStatusCode(http.StatusBadRequest)
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
encoder.Encode(&domain.Error{
|
encoder.Encode(domain.NewError(
|
||||||
Description: err.Error(),
|
domain.ErrorCodeInvalidRequest,
|
||||||
Frame: xerrors.Caller(1),
|
err.Error(),
|
||||||
})
|
"https://indieauth.net/source/#request",
|
||||||
|
))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder.Encode(&ExchangeResponse{
|
resp := &ExchangeResponse{
|
||||||
AccessToken: token.AccessToken,
|
AccessToken: token.AccessToken,
|
||||||
TokenType: "Bearer",
|
TokenType: "Bearer",
|
||||||
Scope: token.Scope.String(),
|
Scope: token.Scope.String(),
|
||||||
Me: token.Me.String(),
|
Me: token.Me.String(),
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if profile == nil {
|
||||||
|
encoder.Encode(resp)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Profile = new(ProfileResponse)
|
||||||
|
|
||||||
|
if len(profile.Name) > 0 {
|
||||||
|
resp.Profile.Name = profile.Name[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(profile.URL) > 0 {
|
||||||
|
resp.Profile.URL = profile.URL[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(profile.Photo) > 0 {
|
||||||
|
resp.Profile.Photo = profile.Photo[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(profile.Email) > 0 {
|
||||||
|
resp.Profile.Email = profile.Email[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = encoder.Encode(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RequestHandler) handleRevoke(ctx *http.RequestCtx) {
|
func (h *RequestHandler) handleRevoke(ctx *http.RequestCtx) {
|
||||||
|
@ -174,20 +210,24 @@ func (h *RequestHandler) handleRevoke(ctx *http.RequestCtx) {
|
||||||
|
|
||||||
req := new(RevokeRequest)
|
req := new(RevokeRequest)
|
||||||
if err := req.bind(ctx); err != nil {
|
if err := req.bind(ctx); err != nil {
|
||||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
|
encoder.Encode(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.tokens.Revoke(ctx, req.Token); err != nil {
|
if err := h.tokens.Revoke(ctx, req.Token); err != nil {
|
||||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
|
encoder.Encode(domain.NewError(
|
||||||
|
domain.ErrorCodeInvalidRequest,
|
||||||
|
err.Error(),
|
||||||
|
"",
|
||||||
|
))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := encoder.Encode(&RevocationResponse{}); err != nil {
|
_ = encoder.Encode(&RevocationResponse{})
|
||||||
ctx.Error(err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RequestHandler) handleTicket(ctx *http.RequestCtx) {
|
func (h *RequestHandler) handleTicket(ctx *http.RequestCtx) {
|
||||||
|
@ -204,53 +244,72 @@ func (h *RequestHandler) handleTicket(ctx *http.RequestCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO(toby3d)
|
t, err := h.tickets.Exchange(ctx, req.Ticket)
|
||||||
token, err := h.tickets.Redeem(ctx, req.Ticket)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SetStatusCode(http.StatusInternalServerError)
|
ctx.SetStatusCode(http.StatusInternalServerError)
|
||||||
encoder.Encode(domain.Error{
|
encoder.Encode(domain.NewError(
|
||||||
Description: err.Error(),
|
domain.ErrorCodeInvalidRequest,
|
||||||
Frame: xerrors.Caller(1),
|
err.Error(),
|
||||||
})
|
"https://indieauth.net/source/#request",
|
||||||
|
))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
encoder.Encode(ExchangeResponse{})
|
encoder.Encode(ExchangeResponse{
|
||||||
|
AccessToken: t.AccessToken,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
Scope: t.Scope.String(),
|
||||||
|
Me: t.Me.String(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ExchangeRequest) bind(ctx *http.RequestCtx) error {
|
func (r *ExchangeRequest) bind(ctx *http.RequestCtx) error {
|
||||||
|
indieAuthError := new(domain.Error)
|
||||||
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
|
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
|
||||||
return domain.Error{
|
if errors.As(err, indieAuthError) {
|
||||||
Code: "invalid_request",
|
return indieAuthError
|
||||||
Description: err.Error(),
|
|
||||||
Frame: xerrors.Caller(1),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return domain.NewError(
|
||||||
|
domain.ErrorCodeInvalidRequest,
|
||||||
|
err.Error(),
|
||||||
|
"https://indieauth.net/source/#request",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RevokeRequest) bind(ctx *http.RequestCtx) error {
|
func (r *RevokeRequest) bind(ctx *http.RequestCtx) error {
|
||||||
|
indieAuthError := new(domain.Error)
|
||||||
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
|
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
|
||||||
return domain.Error{
|
if errors.As(err, indieAuthError) {
|
||||||
Code: "invalid_request",
|
return indieAuthError
|
||||||
Description: err.Error(),
|
|
||||||
Frame: xerrors.Caller(1),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return domain.NewError(
|
||||||
|
domain.ErrorCodeInvalidRequest,
|
||||||
|
err.Error(),
|
||||||
|
"https://indieauth.net/source/#request",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *TicketRequest) bind(ctx *http.RequestCtx) error {
|
func (r *TicketRequest) bind(ctx *http.RequestCtx) error {
|
||||||
|
indieAuthError := new(domain.Error)
|
||||||
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
|
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
|
||||||
return domain.Error{
|
if errors.As(err, indieAuthError) {
|
||||||
Code: "invalid_request",
|
return indieAuthError
|
||||||
Description: err.Error(),
|
|
||||||
Frame: xerrors.Caller(1),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return domain.NewError(
|
||||||
|
domain.ErrorCodeInvalidRequest,
|
||||||
|
err.Error(),
|
||||||
|
"https://indieauth.net/source/#request",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -16,11 +16,19 @@ import (
|
||||||
"source.toby3d.me/website/indieauth/internal/domain"
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
sessionrepo "source.toby3d.me/website/indieauth/internal/session/repository/memory"
|
sessionrepo "source.toby3d.me/website/indieauth/internal/session/repository/memory"
|
||||||
"source.toby3d.me/website/indieauth/internal/testing/httptest"
|
"source.toby3d.me/website/indieauth/internal/testing/httptest"
|
||||||
|
ticketrepo "source.toby3d.me/website/indieauth/internal/ticket/repository/memory"
|
||||||
|
ticketucase "source.toby3d.me/website/indieauth/internal/ticket/usecase"
|
||||||
delivery "source.toby3d.me/website/indieauth/internal/token/delivery/http"
|
delivery "source.toby3d.me/website/indieauth/internal/token/delivery/http"
|
||||||
tokenrepo "source.toby3d.me/website/indieauth/internal/token/repository/memory"
|
tokenrepo "source.toby3d.me/website/indieauth/internal/token/repository/memory"
|
||||||
tokenucase "source.toby3d.me/website/indieauth/internal/token/usecase"
|
tokenucase "source.toby3d.me/website/indieauth/internal/token/usecase"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/* TODO(toby3d)
|
||||||
|
func TestExchange(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
func TestVerification(t *testing.T) {
|
func TestVerification(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -30,8 +38,18 @@ func TestVerification(t *testing.T) {
|
||||||
|
|
||||||
r := router.New()
|
r := router.New()
|
||||||
// TODO(toby3d): provide tickets
|
// TODO(toby3d): provide tickets
|
||||||
delivery.NewRequestHandler(tokenucase.NewTokenUseCase(tokenrepo.NewMemoryTokenRepository(store),
|
delivery.NewRequestHandler(
|
||||||
sessionrepo.NewMemorySessionRepository(config, store), config)).Register(r)
|
tokenucase.NewTokenUseCase(
|
||||||
|
tokenrepo.NewMemoryTokenRepository(store),
|
||||||
|
sessionrepo.NewMemorySessionRepository(config, store),
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
ticketucase.NewTicketUseCase(
|
||||||
|
ticketrepo.NewMemoryTicketRepository(store, config),
|
||||||
|
new(http.Client),
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
).Register(r)
|
||||||
|
|
||||||
client, _, cleanup := httptest.New(t, r.Handler)
|
client, _, cleanup := httptest.New(t, r.Handler)
|
||||||
t.Cleanup(cleanup)
|
t.Cleanup(cleanup)
|
||||||
|
@ -63,8 +81,18 @@ func TestRevocation(t *testing.T) {
|
||||||
accessToken := domain.TestToken(t)
|
accessToken := domain.TestToken(t)
|
||||||
|
|
||||||
r := router.New()
|
r := router.New()
|
||||||
delivery.NewRequestHandler(tokenucase.NewTokenUseCase(tokens, sessionrepo.NewMemorySessionRepository(config,
|
delivery.NewRequestHandler(
|
||||||
store), config)).Register(r)
|
tokenucase.NewTokenUseCase(
|
||||||
|
tokens,
|
||||||
|
sessionrepo.NewMemorySessionRepository(config, store),
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
ticketucase.NewTicketUseCase(
|
||||||
|
ticketrepo.NewMemoryTicketRepository(store, config),
|
||||||
|
new(http.Client),
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
).Register(r)
|
||||||
|
|
||||||
client, _, cleanup := httptest.New(t, r.Handler)
|
client, _, cleanup := httptest.New(t, r.Handler)
|
||||||
t.Cleanup(cleanup)
|
t.Cleanup(cleanup)
|
||||||
|
|
|
@ -2,7 +2,6 @@ package token
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"source.toby3d.me/website/indieauth/internal/domain"
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
)
|
)
|
||||||
|
@ -13,6 +12,6 @@ type Repository interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrExist = errors.New("token already exist")
|
ErrExist error = domain.NewError(domain.ErrorCodeServerError, "token already exist", "")
|
||||||
ErrNotExist = errors.New("token not exist")
|
ErrNotExist error = domain.NewError(domain.ErrorCodeServerError, "token not exist", "")
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,6 @@ package token
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"source.toby3d.me/website/indieauth/internal/domain"
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
)
|
)
|
||||||
|
@ -16,7 +15,7 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
UseCase interface {
|
UseCase interface {
|
||||||
Exchange(ctx context.Context, opts ExchangeOptions) (*domain.Token, error)
|
Exchange(ctx context.Context, opts ExchangeOptions) (*domain.Token, *domain.Profile, error)
|
||||||
|
|
||||||
// Verify checks the AccessToken and returns the associated information.
|
// Verify checks the AccessToken and returns the associated information.
|
||||||
Verify(ctx context.Context, accessToken string) (*domain.Token, error)
|
Verify(ctx context.Context, accessToken string) (*domain.Token, error)
|
||||||
|
@ -26,4 +25,31 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrRevoke = errors.New("this token has been revoked")
|
var (
|
||||||
|
ErrRevoke error = domain.NewError(
|
||||||
|
domain.ErrorCodeAccessDenied,
|
||||||
|
"this token has been revoked",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
ErrMismatchClientID error = domain.NewError(
|
||||||
|
domain.ErrorCodeInvalidRequest,
|
||||||
|
"client's URL MUST match the client_id used in the authentication request",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
ErrMismatchRedirectURI error = domain.NewError(
|
||||||
|
domain.ErrorCodeInvalidRequest,
|
||||||
|
"client's redirect URL MUST match the initial authentication request",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
ErrEmptyScope error = domain.NewError(
|
||||||
|
domain.ErrorCodeInvalidScope,
|
||||||
|
"empty scopes are invalid",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
ErrMismatchPKCE error = domain.NewError(
|
||||||
|
domain.ErrorCodeInvalidRequest,
|
||||||
|
"code_verifier is not hashes to the same value as given in the code_challenge in the original "+
|
||||||
|
" authorization request",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -19,12 +19,9 @@ type tokenUseCase struct {
|
||||||
tokens token.Repository
|
tokens token.Repository
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint: gochecknoinits
|
|
||||||
func init() {
|
|
||||||
jwt.RegisterCustomField("scope", make(domain.Scopes, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenUseCase(tokens token.Repository, sessions session.Repository, config *domain.Config) token.UseCase {
|
func NewTokenUseCase(tokens token.Repository, sessions session.Repository, config *domain.Config) token.UseCase {
|
||||||
|
jwt.RegisterCustomField("scope", make(domain.Scopes, 0))
|
||||||
|
|
||||||
return &tokenUseCase{
|
return &tokenUseCase{
|
||||||
sessions: sessions,
|
sessions: sessions,
|
||||||
config: config,
|
config: config,
|
||||||
|
@ -32,39 +29,31 @@ func NewTokenUseCase(tokens token.Repository, sessions session.Repository, confi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (useCase *tokenUseCase) Exchange(ctx context.Context, opts token.ExchangeOptions) (*domain.Token, error) {
|
func (useCase *tokenUseCase) Exchange(ctx context.Context, opts token.ExchangeOptions) (*domain.Token, *domain.Profile,
|
||||||
|
error) {
|
||||||
session, err := useCase.sessions.GetAndDelete(ctx, opts.Code)
|
session, err := useCase.sessions.GetAndDelete(ctx, opts.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot get session from store: %w", err)
|
return nil, nil, fmt.Errorf("cannot get session from store: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.ClientID.String() != session.ClientID.String() {
|
if opts.ClientID.String() != session.ClientID.String() {
|
||||||
return nil, domain.Error{
|
return nil, nil, token.ErrMismatchClientID
|
||||||
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() {
|
if opts.RedirectURI.String() != session.RedirectURI.String() {
|
||||||
return nil, domain.Error{
|
return nil, nil, token.ErrMismatchRedirectURI
|
||||||
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 != "" &&
|
if session.CodeChallenge != "" &&
|
||||||
!session.CodeChallengeMethod.Validate(session.CodeChallenge, opts.CodeVerifier) {
|
!session.CodeChallengeMethod.Validate(session.CodeChallenge, opts.CodeVerifier) {
|
||||||
return nil, domain.Error{
|
return nil, nil, token.ErrMismatchPKCE
|
||||||
Code: "invalid_request",
|
}
|
||||||
Description: "code_verifier is not hashes to the same value as given in " +
|
|
||||||
"the code_challenge in the original authorization request",
|
// NOTE(toby3d): If the authorization code was issued with no scope, the
|
||||||
URI: "https://indieauth.net/source/#request",
|
// token endpoint MUST NOT issue an access token, as empty scopes are
|
||||||
Frame: xerrors.Caller(1),
|
// invalid per Section 3.3 of OAuth 2.0 RFC6749.
|
||||||
}
|
if session.Scope.IsEmpty() {
|
||||||
|
return nil, nil, token.ErrEmptyScope
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := domain.NewToken(domain.NewTokenOptions{
|
t, err := domain.NewToken(domain.NewTokenOptions{
|
||||||
|
@ -77,10 +66,18 @@ func (useCase *tokenUseCase) Exchange(ctx context.Context, opts token.ExchangeOp
|
||||||
Subject: session.Me,
|
Subject: session.Me,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot generate a new access token: %w", err)
|
return nil, nil, fmt.Errorf("cannot generate a new access token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return t, nil
|
if !session.Scope.Has(domain.ScopeProfile) {
|
||||||
|
return t, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p := new(domain.Profile)
|
||||||
|
|
||||||
|
// TODO(toby3d): if session.Scope.Has(domain.ScopeEmail) {}
|
||||||
|
|
||||||
|
return t, p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (useCase *tokenUseCase) Verify(ctx context.Context, accessToken string) (*domain.Token, error) {
|
func (useCase *tokenUseCase) Verify(ctx context.Context, accessToken string) (*domain.Token, error) {
|
||||||
|
|
|
@ -14,9 +14,11 @@ import (
|
||||||
usecase "source.toby3d.me/website/indieauth/internal/token/usecase"
|
usecase "source.toby3d.me/website/indieauth/internal/token/usecase"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/* TODO(toby3d)
|
||||||
func TestExchange(t *testing.T) {
|
func TestExchange(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
func TestVerify(t *testing.T) {
|
func TestVerify(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
Loading…
Reference in New Issue