228 lines
5.4 KiB
Go
228 lines
5.4 KiB
Go
package http
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/goccy/go-json"
|
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
|
|
|
"source.toby3d.me/toby3d/auth/internal/common"
|
|
"source.toby3d.me/toby3d/auth/internal/domain"
|
|
"source.toby3d.me/toby3d/auth/internal/middleware"
|
|
"source.toby3d.me/toby3d/auth/internal/token"
|
|
"source.toby3d.me/toby3d/auth/internal/urlutil"
|
|
)
|
|
|
|
type Handler struct {
|
|
config domain.Config
|
|
tokens token.UseCase
|
|
}
|
|
|
|
func NewHandler(tokens token.UseCase, config domain.Config) *Handler {
|
|
return &Handler{
|
|
config: config,
|
|
tokens: tokens,
|
|
}
|
|
}
|
|
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
chain := middleware.Chain{
|
|
//nolint:exhaustivestruct
|
|
middleware.JWTWithConfig(middleware.JWTConfig{
|
|
Skipper: func(_ http.ResponseWriter, r *http.Request) bool {
|
|
head, _ := urlutil.ShiftPath(r.URL.Path)
|
|
|
|
return head == "token"
|
|
},
|
|
SigningKey: []byte(h.config.JWT.Secret),
|
|
SigningMethod: jwa.SignatureAlgorithm(h.config.JWT.Algorithm),
|
|
ContextKey: "token",
|
|
TokenLookup: "form:token," + "header:" + common.HeaderAuthorization + ":Bearer ",
|
|
AuthScheme: "Bearer",
|
|
}),
|
|
}
|
|
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
}
|
|
|
|
var head string
|
|
head, r.URL.Path = urlutil.ShiftPath(r.URL.Path)
|
|
|
|
switch head {
|
|
default:
|
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
case "token":
|
|
chain.Handler(h.handleAction).ServeHTTP(w, r)
|
|
case "introspect":
|
|
chain.Handler(h.handleIntrospect).ServeHTTP(w, r)
|
|
case "revocation":
|
|
chain.Handler(h.handleRevokation).ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleIntrospect(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
}
|
|
|
|
w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
|
|
|
|
encoder := json.NewEncoder(w)
|
|
|
|
req := new(TokenIntrospectRequest)
|
|
if err := req.bind(r); err != nil {
|
|
_ = encoder.Encode(err)
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
return
|
|
}
|
|
|
|
tkn, _, err := h.tokens.Verify(r.Context(), req.Token)
|
|
if err != nil || tkn == nil {
|
|
// WARN(toby3d): If the token is not valid, the endpoint still
|
|
// MUST return a 200 Response.
|
|
_ = encoder.Encode(&TokenInvalidIntrospectResponse{Active: false})
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
return
|
|
}
|
|
|
|
_ = encoder.Encode(&TokenIntrospectResponse{
|
|
Active: true,
|
|
ClientID: tkn.ClientID.String(),
|
|
Exp: tkn.Expiry.Unix(),
|
|
Iat: tkn.CreatedAt.Unix(),
|
|
Me: tkn.Me.String(),
|
|
Scope: tkn.Scope.String(),
|
|
})
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func (h *Handler) handleAction(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
}
|
|
|
|
w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
|
|
|
|
encoder := json.NewEncoder(w)
|
|
|
|
switch {
|
|
case r.PostForm.Has("grant_type"):
|
|
h.handleExchange(w, r)
|
|
case r.PostForm.Has("action"):
|
|
if err := r.ParseForm(); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
_ = encoder.Encode(domain.NewError(domain.ErrorCodeInvalidRequest, err.Error(), ""))
|
|
|
|
return
|
|
}
|
|
|
|
action, err := domain.ParseAction(r.PostForm.Get("action"))
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
_ = encoder.Encode(domain.NewError(domain.ErrorCodeInvalidRequest, err.Error(), ""))
|
|
|
|
return
|
|
}
|
|
|
|
switch action {
|
|
case domain.ActionRevoke:
|
|
h.handleRevokation(w, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleExchange(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
}
|
|
|
|
w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
|
|
|
|
encoder := json.NewEncoder(w)
|
|
|
|
req := new(TokenExchangeRequest)
|
|
if err := req.bind(r); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
_ = encoder.Encode(err)
|
|
|
|
return
|
|
}
|
|
|
|
token, profile, err := h.tokens.Exchange(r.Context(), token.ExchangeOptions{
|
|
ClientID: req.ClientID,
|
|
RedirectURI: req.RedirectURI.URL,
|
|
Code: req.Code,
|
|
CodeVerifier: req.CodeVerifier,
|
|
})
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
_ = encoder.Encode(domain.NewError(domain.ErrorCodeInvalidRequest, err.Error(),
|
|
"https://indieauth.net/source/#request"))
|
|
|
|
return
|
|
}
|
|
|
|
resp := &TokenExchangeResponse{
|
|
AccessToken: token.AccessToken,
|
|
ExpiresIn: token.Expiry.Unix(),
|
|
Me: token.Me.String(),
|
|
Profile: NewTokenProfileResponse(profile),
|
|
RefreshToken: "", // TODO(toby3d)
|
|
}
|
|
|
|
_ = encoder.Encode(resp)
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func (h *Handler) handleRevokation(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
}
|
|
|
|
w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
|
|
|
|
encoder := json.NewEncoder(w)
|
|
|
|
req := NewTokenRevocationRequest()
|
|
if err := req.bind(r); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
_ = encoder.Encode(err)
|
|
|
|
return
|
|
}
|
|
|
|
if err := h.tokens.Revoke(r.Context(), req.Token); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
_ = encoder.Encode(domain.NewError(domain.ErrorCodeInvalidRequest, err.Error(), ""))
|
|
|
|
return
|
|
}
|
|
|
|
_ = encoder.Encode(&TokenRevocationResponse{})
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|