2021-12-29 20:20:14 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
2022-01-29 20:30:37 +00:00
|
|
|
"errors"
|
2021-12-29 20:20:14 +00:00
|
|
|
"fmt"
|
2022-01-20 19:45:26 +00:00
|
|
|
"path"
|
2021-12-29 20:20:14 +00:00
|
|
|
|
|
|
|
"github.com/fasthttp/router"
|
2022-01-20 19:45:26 +00:00
|
|
|
"github.com/goccy/go-json"
|
2022-06-09 19:14:21 +00:00
|
|
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
2021-12-29 20:20:14 +00:00
|
|
|
http "github.com/valyala/fasthttp"
|
2022-01-20 19:45:26 +00:00
|
|
|
"golang.org/x/text/language"
|
|
|
|
"golang.org/x/text/message"
|
2021-12-29 20:20:14 +00:00
|
|
|
|
2022-03-13 10:58:34 +00:00
|
|
|
"source.toby3d.me/toby3d/auth/internal/common"
|
|
|
|
"source.toby3d.me/toby3d/auth/internal/domain"
|
|
|
|
"source.toby3d.me/toby3d/auth/internal/random"
|
|
|
|
"source.toby3d.me/toby3d/auth/internal/ticket"
|
|
|
|
"source.toby3d.me/toby3d/auth/web"
|
2022-03-25 18:07:52 +00:00
|
|
|
"source.toby3d.me/toby3d/form"
|
|
|
|
"source.toby3d.me/toby3d/middleware"
|
2021-12-29 20:20:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type (
|
2022-02-01 17:27:48 +00:00
|
|
|
TicketGenerateRequest struct {
|
2022-01-20 19:45:26 +00:00
|
|
|
// The access token should be used when acting on behalf of this URL.
|
2022-01-29 14:54:37 +00:00
|
|
|
Subject *domain.Me `form:"subject"`
|
2021-12-29 20:20:14 +00:00
|
|
|
|
|
|
|
// The access token will work at this URL.
|
|
|
|
Resource *domain.URL `form:"resource"`
|
2022-01-20 19:45:26 +00:00
|
|
|
}
|
|
|
|
|
2022-02-01 17:27:48 +00:00
|
|
|
TicketExchangeRequest struct {
|
2022-01-20 19:45:26 +00:00
|
|
|
// A random string that can be redeemed for an access token.
|
|
|
|
Ticket string `form:"ticket"`
|
2021-12-29 20:20:14 +00:00
|
|
|
|
|
|
|
// The access token should be used when acting on behalf of this URL.
|
2022-01-29 14:54:37 +00:00
|
|
|
Subject *domain.Me `form:"subject"`
|
2022-01-20 19:45:26 +00:00
|
|
|
|
|
|
|
// The access token will work at this URL.
|
|
|
|
Resource *domain.URL `form:"resource"`
|
2021-12-29 20:20:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
RequestHandler struct {
|
2022-01-20 19:45:26 +00:00
|
|
|
config *domain.Config
|
|
|
|
matcher language.Matcher
|
|
|
|
tickets ticket.UseCase
|
2021-12-29 20:20:14 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2022-01-20 19:45:26 +00:00
|
|
|
func NewRequestHandler(tickets ticket.UseCase, matcher language.Matcher, config *domain.Config) *RequestHandler {
|
2021-12-29 20:20:14 +00:00
|
|
|
return &RequestHandler{
|
2022-01-20 19:45:26 +00:00
|
|
|
config: config,
|
|
|
|
matcher: matcher,
|
|
|
|
tickets: tickets,
|
2021-12-29 20:20:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *RequestHandler) Register(r *router.Router) {
|
2022-03-25 18:07:52 +00:00
|
|
|
//nolint: exhaustivestruct
|
2022-01-20 19:45:26 +00:00
|
|
|
chain := middleware.Chain{
|
|
|
|
middleware.CSRFWithConfig(middleware.CSRFConfig{
|
2022-02-01 17:27:48 +00:00
|
|
|
Skipper: func(ctx *http.RequestCtx) bool {
|
|
|
|
matched, _ := path.Match("/ticket*", string(ctx.Path()))
|
|
|
|
|
|
|
|
return ctx.IsPost() && matched
|
|
|
|
},
|
|
|
|
CookieMaxAge: 0,
|
2022-02-07 20:34:03 +00:00
|
|
|
CookieSameSite: http.CookieSameSiteStrictMode,
|
2022-01-20 19:45:26 +00:00
|
|
|
ContextKey: "csrf",
|
2022-02-07 20:34:03 +00:00
|
|
|
CookieDomain: h.config.Server.Domain,
|
|
|
|
CookieName: "__Secure-csrf",
|
2022-06-22 14:22:19 +00:00
|
|
|
CookiePath: "/ticket",
|
2022-01-20 19:45:26 +00:00
|
|
|
TokenLookup: "form:_csrf",
|
2022-02-01 17:27:48 +00:00
|
|
|
TokenLength: 0,
|
2022-01-20 19:45:26 +00:00
|
|
|
CookieSecure: true,
|
|
|
|
CookieHTTPOnly: true,
|
2022-02-01 17:27:48 +00:00
|
|
|
}),
|
|
|
|
middleware.JWTWithConfig(middleware.JWTConfig{
|
|
|
|
AuthScheme: "Bearer",
|
|
|
|
BeforeFunc: nil,
|
|
|
|
Claims: nil,
|
2022-02-16 23:33:56 +00:00
|
|
|
ContextKey: "token",
|
2022-02-01 17:27:48 +00:00
|
|
|
ErrorHandler: nil,
|
|
|
|
ErrorHandlerWithContext: nil,
|
|
|
|
ParseTokenFunc: nil,
|
|
|
|
SigningKey: []byte(h.config.JWT.Secret),
|
|
|
|
SigningKeys: nil,
|
|
|
|
SigningMethod: jwa.SignatureAlgorithm(h.config.JWT.Algorithm),
|
|
|
|
Skipper: middleware.DefaultSkipper,
|
|
|
|
SuccessHandler: nil,
|
2022-02-25 23:18:04 +00:00
|
|
|
TokenLookup: "header:" + http.HeaderAuthorization +
|
|
|
|
"," + "cookie:" + "__Secure-auth-token",
|
2022-01-20 19:45:26 +00:00
|
|
|
}),
|
|
|
|
middleware.LogFmt(),
|
|
|
|
}
|
2022-02-01 17:27:48 +00:00
|
|
|
|
2022-01-20 19:45:26 +00:00
|
|
|
r.GET("/ticket", chain.RequestHandler(h.handleRender))
|
|
|
|
r.POST("/api/ticket", chain.RequestHandler(h.handleSend))
|
2022-01-29 20:30:37 +00:00
|
|
|
r.POST("/ticket", chain.RequestHandler(h.handleRedeem))
|
2022-01-20 19:45:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (h *RequestHandler) handleRender(ctx *http.RequestCtx) {
|
|
|
|
ctx.SetContentType(common.MIMETextHTMLCharsetUTF8)
|
|
|
|
|
|
|
|
tags, _, _ := language.ParseAcceptLanguage(string(ctx.Request.Header.Peek(http.HeaderAcceptLanguage)))
|
|
|
|
tag, _, _ := h.matcher.Match(tags...)
|
2022-01-31 16:15:38 +00:00
|
|
|
baseOf := web.BaseOf{
|
|
|
|
Config: h.config,
|
|
|
|
Language: tag,
|
|
|
|
Printer: message.NewPrinter(tag),
|
|
|
|
}
|
2022-01-20 19:45:26 +00:00
|
|
|
|
|
|
|
csrf, _ := ctx.UserValue("csrf").([]byte)
|
|
|
|
web.WriteTemplate(ctx, &web.TicketPage{
|
2022-01-31 16:15:38 +00:00
|
|
|
BaseOf: baseOf,
|
|
|
|
CSRF: csrf,
|
2022-01-20 19:45:26 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *RequestHandler) handleSend(ctx *http.RequestCtx) {
|
2022-02-07 20:34:03 +00:00
|
|
|
ctx.Response.Header.Set(http.HeaderAccessControlAllowOrigin, h.config.Server.Domain)
|
2022-01-20 19:45:26 +00:00
|
|
|
ctx.SetContentType(common.MIMEApplicationJSONCharsetUTF8)
|
|
|
|
ctx.SetStatusCode(http.StatusOK)
|
|
|
|
|
|
|
|
encoder := json.NewEncoder(ctx)
|
|
|
|
|
2022-02-01 17:27:48 +00:00
|
|
|
req := new(TicketGenerateRequest)
|
2022-01-20 19:45:26 +00:00
|
|
|
if err := req.bind(ctx); err != nil {
|
|
|
|
ctx.SetStatusCode(http.StatusBadRequest)
|
2022-02-01 17:27:48 +00:00
|
|
|
|
|
|
|
_ = encoder.Encode(err)
|
2022-01-20 19:45:26 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ticket := &domain.Ticket{
|
|
|
|
Ticket: "",
|
|
|
|
Resource: req.Resource,
|
|
|
|
Subject: req.Subject,
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
if ticket.Ticket, err = random.String(h.config.TicketAuth.Length); err != nil {
|
|
|
|
ctx.SetStatusCode(http.StatusInternalServerError)
|
2022-02-01 17:27:48 +00:00
|
|
|
|
|
|
|
_ = encoder.Encode(domain.NewError(domain.ErrorCodeServerError, err.Error(), ""))
|
2022-01-20 19:45:26 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = h.tickets.Generate(ctx, ticket); err != nil {
|
|
|
|
ctx.SetStatusCode(http.StatusInternalServerError)
|
2022-02-01 17:27:48 +00:00
|
|
|
|
|
|
|
_ = encoder.Encode(domain.NewError(domain.ErrorCodeServerError, err.Error(), ""))
|
2022-01-20 19:45:26 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.SetStatusCode(http.StatusOK)
|
2021-12-29 20:20:14 +00:00
|
|
|
}
|
|
|
|
|
2022-01-29 20:30:37 +00:00
|
|
|
func (h *RequestHandler) handleRedeem(ctx *http.RequestCtx) {
|
2021-12-29 20:20:14 +00:00
|
|
|
ctx.SetContentType(common.MIMEApplicationJSONCharsetUTF8)
|
|
|
|
ctx.SetStatusCode(http.StatusOK)
|
|
|
|
|
|
|
|
encoder := json.NewEncoder(ctx)
|
|
|
|
|
2022-02-01 17:27:48 +00:00
|
|
|
req := new(TicketExchangeRequest)
|
2021-12-29 20:20:14 +00:00
|
|
|
if err := req.bind(ctx); err != nil {
|
|
|
|
ctx.SetStatusCode(http.StatusBadRequest)
|
2022-02-01 17:27:48 +00:00
|
|
|
|
|
|
|
_ = encoder.Encode(err)
|
2021-12-29 20:20:14 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-29 20:30:37 +00:00
|
|
|
token, err := h.tickets.Redeem(ctx, &domain.Ticket{
|
2021-12-30 00:27:52 +00:00
|
|
|
Ticket: req.Ticket,
|
|
|
|
Resource: req.Resource,
|
|
|
|
Subject: req.Subject,
|
|
|
|
})
|
2021-12-29 20:20:14 +00:00
|
|
|
if err != nil {
|
|
|
|
ctx.SetStatusCode(http.StatusBadRequest)
|
2022-02-01 17:27:48 +00:00
|
|
|
|
|
|
|
_ = encoder.Encode(domain.NewError(domain.ErrorCodeServerError, err.Error(), ""))
|
2021-12-29 20:20:14 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(toby3d): print the result as part of the debugging. Instead, we
|
|
|
|
// need to send or save the token to the recipient for later use.
|
|
|
|
ctx.SetBodyString(fmt.Sprintf(`{
|
|
|
|
"access_token": "%s",
|
|
|
|
"token_type": "Bearer",
|
|
|
|
"scope": "%s",
|
|
|
|
"me": "%s"
|
|
|
|
}`, token.AccessToken, token.Scope.String(), token.Me.String()))
|
|
|
|
}
|
|
|
|
|
2022-02-01 17:27:48 +00:00
|
|
|
func (req *TicketGenerateRequest) bind(ctx *http.RequestCtx) (err error) {
|
2022-01-29 20:30:37 +00:00
|
|
|
indieAuthError := new(domain.Error)
|
2022-06-09 19:14:21 +00:00
|
|
|
if err = form.Unmarshal(ctx.Request.PostArgs().QueryString(), req); err != nil {
|
2022-01-29 20:30:37 +00:00
|
|
|
if errors.As(err, indieAuthError) {
|
|
|
|
return indieAuthError
|
2022-01-20 19:45:26 +00:00
|
|
|
}
|
2022-01-29 20:30:37 +00:00
|
|
|
|
|
|
|
return domain.NewError(
|
|
|
|
domain.ErrorCodeInvalidRequest,
|
|
|
|
err.Error(),
|
|
|
|
"https://indieweb.org/IndieAuth_Ticket_Auth#Create_the_IndieAuth_ticket",
|
|
|
|
)
|
2022-01-20 19:45:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if req.Resource == nil {
|
2022-01-29 20:30:37 +00:00
|
|
|
return domain.NewError(
|
|
|
|
domain.ErrorCodeInvalidRequest,
|
|
|
|
"resource value MUST be set",
|
|
|
|
"https://indieweb.org/IndieAuth_Ticket_Auth#Create_the_IndieAuth_ticket",
|
|
|
|
)
|
2022-01-20 19:45:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if req.Subject == nil {
|
2022-01-29 20:30:37 +00:00
|
|
|
return domain.NewError(
|
|
|
|
domain.ErrorCodeInvalidRequest,
|
|
|
|
"subject value MUST be set",
|
|
|
|
"https://indieweb.org/IndieAuth_Ticket_Auth#Create_the_IndieAuth_ticket",
|
|
|
|
)
|
2022-01-20 19:45:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-09 19:14:21 +00:00
|
|
|
func (req *TicketExchangeRequest) bind(ctx *http.RequestCtx) error {
|
2022-01-29 20:30:37 +00:00
|
|
|
indieAuthError := new(domain.Error)
|
2022-06-09 19:14:21 +00:00
|
|
|
if err := form.Unmarshal(ctx.Request.PostArgs().QueryString(), req); err != nil {
|
2022-01-29 20:30:37 +00:00
|
|
|
if errors.As(err, indieAuthError) {
|
|
|
|
return indieAuthError
|
2021-12-29 20:20:14 +00:00
|
|
|
}
|
2022-01-29 20:30:37 +00:00
|
|
|
|
|
|
|
return domain.NewError(
|
|
|
|
domain.ErrorCodeInvalidRequest,
|
|
|
|
err.Error(),
|
|
|
|
"https://indieweb.org/IndieAuth_Ticket_Auth#Create_the_IndieAuth_ticket",
|
|
|
|
)
|
2021-12-29 20:20:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if req.Ticket == "" {
|
2022-01-29 20:30:37 +00:00
|
|
|
return domain.NewError(
|
|
|
|
domain.ErrorCodeInvalidRequest,
|
|
|
|
"ticket parameter is required",
|
|
|
|
"https://indieweb.org/IndieAuth_Ticket_Auth#Create_the_IndieAuth_ticket",
|
|
|
|
)
|
2021-12-29 20:20:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if req.Resource == nil {
|
2022-01-29 20:30:37 +00:00
|
|
|
return domain.NewError(
|
|
|
|
domain.ErrorCodeInvalidRequest,
|
|
|
|
"resource parameter is required",
|
|
|
|
"https://indieweb.org/IndieAuth_Ticket_Auth#Create_the_IndieAuth_ticket",
|
|
|
|
)
|
2021-12-29 20:20:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if req.Subject == nil {
|
2022-01-29 20:30:37 +00:00
|
|
|
return domain.NewError(
|
|
|
|
domain.ErrorCodeInvalidRequest,
|
|
|
|
"subject parameter is required",
|
|
|
|
"https://indieweb.org/IndieAuth_Ticket_Auth#Create_the_IndieAuth_ticket",
|
|
|
|
)
|
2021-12-29 20:20:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|