auth/internal/token/delivery/http/token_http.go

317 lines
7.0 KiB
Go

package http
import (
"errors"
"strings"
"github.com/fasthttp/router"
json "github.com/goccy/go-json"
http "github.com/valyala/fasthttp"
"source.toby3d.me/toby3d/form"
"source.toby3d.me/toby3d/middleware"
"source.toby3d.me/website/indieauth/internal/common"
"source.toby3d.me/website/indieauth/internal/domain"
"source.toby3d.me/website/indieauth/internal/ticket"
"source.toby3d.me/website/indieauth/internal/token"
)
type (
ExchangeRequest struct {
ClientID *domain.ClientID `form:"client_id"`
RedirectURI *domain.URL `form:"redirect_uri"`
GrantType domain.GrantType `form:"grant_type"`
Code string `form:"code"`
CodeVerifier string `form:"code_verifier"`
}
RevokeRequest struct {
Action domain.Action `form:"action"`
Token string `form:"token"`
}
TicketRequest struct {
Action domain.Action `form:"action"`
Ticket string `form:"ticket"`
}
//nolint: tagliatelle
ExchangeResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
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
VerificationResponse struct {
Me *domain.Me `json:"me"`
ClientID *domain.ClientID `json:"client_id"`
Scope domain.Scopes `json:"scope"`
}
RevocationResponse struct{}
RequestHandler struct {
tokens token.UseCase
tickets ticket.UseCase
}
)
func NewRequestHandler(tokens token.UseCase, tickets ticket.UseCase) *RequestHandler {
return &RequestHandler{
tokens: tokens,
tickets: tickets,
}
}
func (h *RequestHandler) Register(r *router.Router) {
chain := middleware.Chain{
middleware.LogFmt(),
}
r.GET("/token", chain.RequestHandler(h.handleValidate))
r.POST("/token", chain.RequestHandler(h.handleAction))
}
func (h *RequestHandler) handleValidate(ctx *http.RequestCtx) {
ctx.SetContentType(common.MIMEApplicationJSONCharsetUTF8)
ctx.SetStatusCode(http.StatusOK)
encoder := json.NewEncoder(ctx)
t, err := h.tokens.Verify(ctx, strings.TrimPrefix(string(ctx.Request.Header.Peek(http.HeaderAuthorization)),
"Bearer "))
if err != nil || t == nil {
ctx.SetStatusCode(http.StatusUnauthorized)
encoder.Encode(domain.NewError(
domain.ErrorCodeUnauthorizedClient,
err.Error(),
"https://indieauth.net/source/#access-token-verification",
))
return
}
_ = encoder.Encode(&VerificationResponse{
ClientID: t.ClientID,
Me: t.Me,
Scope: t.Scope,
})
}
func (h *RequestHandler) handleAction(ctx *http.RequestCtx) {
ctx.SetContentType(common.MIMEApplicationJSONCharsetUTF8)
encoder := json.NewEncoder(ctx)
switch {
case ctx.PostArgs().Has("grant_type"):
h.handleExchange(ctx)
case ctx.PostArgs().Has("action"):
action, err := domain.ParseAction(string(ctx.PostArgs().Peek("action")))
if err != nil {
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"",
))
return
}
switch action {
case domain.ActionRevoke:
h.handleRevoke(ctx)
case domain.ActionTicket:
h.handleTicket(ctx)
}
}
}
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
}
token, profile, err := h.tokens.Exchange(ctx, token.ExchangeOptions{
ClientID: req.ClientID,
RedirectURI: req.RedirectURI,
Code: req.Code,
CodeVerifier: req.CodeVerifier,
})
if err != nil {
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#request",
))
return
}
resp := &ExchangeResponse{
AccessToken: token.AccessToken,
TokenType: "Bearer",
Scope: token.Scope.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) {
ctx.SetContentType(common.MIMEApplicationJSONCharsetUTF8)
ctx.SetStatusCode(http.StatusOK)
encoder := json.NewEncoder(ctx)
req := new(RevokeRequest)
if err := req.bind(ctx); err != nil {
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(err)
return
}
if err := h.tokens.Revoke(ctx, req.Token); err != nil {
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"",
))
return
}
_ = encoder.Encode(&RevocationResponse{})
}
func (h *RequestHandler) handleTicket(ctx *http.RequestCtx) {
ctx.SetContentType(common.MIMEApplicationJSONCharsetUTF8)
ctx.SetStatusCode(http.StatusOK)
encoder := json.NewEncoder(ctx)
req := new(TicketRequest)
if err := req.bind(ctx); err != nil {
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(err)
return
}
t, err := h.tickets.Exchange(ctx, req.Ticket)
if err != nil {
ctx.SetStatusCode(http.StatusInternalServerError)
encoder.Encode(domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#request",
))
return
}
encoder.Encode(ExchangeResponse{
AccessToken: t.AccessToken,
TokenType: "Bearer",
Scope: t.Scope.String(),
Me: t.Me.String(),
})
}
func (r *ExchangeRequest) bind(ctx *http.RequestCtx) error {
indieAuthError := new(domain.Error)
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
if errors.As(err, indieAuthError) {
return indieAuthError
}
return domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#request",
)
}
return nil
}
func (r *RevokeRequest) bind(ctx *http.RequestCtx) error {
indieAuthError := new(domain.Error)
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
if errors.As(err, indieAuthError) {
return indieAuthError
}
return domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#request",
)
}
return nil
}
func (r *TicketRequest) bind(ctx *http.RequestCtx) error {
indieAuthError := new(domain.Error)
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
if errors.As(err, indieAuthError) {
return indieAuthError
}
return domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#request",
)
}
return nil
}