2021-07-21 23:04:00 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
2022-01-20 23:33:15 +00:00
|
|
|
"crypto/subtle"
|
2023-01-14 21:27:37 +00:00
|
|
|
"net/http"
|
2022-01-13 20:49:41 +00:00
|
|
|
"strings"
|
2021-07-21 23:04:00 +00:00
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
"github.com/goccy/go-json"
|
2022-01-13 20:49:41 +00:00
|
|
|
"golang.org/x/text/language"
|
|
|
|
"golang.org/x/text/message"
|
|
|
|
|
2022-03-13 10:58:34 +00:00
|
|
|
"source.toby3d.me/toby3d/auth/internal/auth"
|
|
|
|
"source.toby3d.me/toby3d/auth/internal/client"
|
|
|
|
"source.toby3d.me/toby3d/auth/internal/common"
|
|
|
|
"source.toby3d.me/toby3d/auth/internal/domain"
|
2023-01-14 21:27:37 +00:00
|
|
|
"source.toby3d.me/toby3d/auth/internal/middleware"
|
2022-03-13 10:58:34 +00:00
|
|
|
"source.toby3d.me/toby3d/auth/internal/profile"
|
2023-01-14 21:27:37 +00:00
|
|
|
"source.toby3d.me/toby3d/auth/internal/urlutil"
|
2022-03-13 10:58:34 +00:00
|
|
|
"source.toby3d.me/toby3d/auth/web"
|
2021-07-21 23:04:00 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type (
|
2023-01-14 21:27:37 +00:00
|
|
|
NewHandlerOptions struct {
|
2022-02-17 16:10:52 +00:00
|
|
|
Auth auth.UseCase
|
|
|
|
Clients client.UseCase
|
2023-01-14 21:27:37 +00:00
|
|
|
Config domain.Config
|
2022-02-17 16:10:52 +00:00
|
|
|
Matcher language.Matcher
|
|
|
|
Profiles profile.UseCase
|
2022-01-13 20:49:41 +00:00
|
|
|
}
|
2021-07-21 23:04:00 +00:00
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
Handler struct {
|
2022-02-01 17:27:48 +00:00
|
|
|
clients client.UseCase
|
2023-01-14 21:27:37 +00:00
|
|
|
config domain.Config
|
2022-02-01 17:27:48 +00:00
|
|
|
matcher language.Matcher
|
|
|
|
useCase auth.UseCase
|
2022-01-13 20:49:41 +00:00
|
|
|
}
|
|
|
|
)
|
2021-07-21 23:04:00 +00:00
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
func NewHandler(opts NewHandlerOptions) *Handler {
|
|
|
|
return &Handler{
|
2022-02-01 17:27:48 +00:00
|
|
|
clients: opts.Clients,
|
|
|
|
config: opts.Config,
|
|
|
|
matcher: opts.Matcher,
|
|
|
|
useCase: opts.Auth,
|
2021-07-21 23:04:00 +00:00
|
|
|
}
|
2022-01-13 20:49:41 +00:00
|
|
|
}
|
2021-07-21 23:04:00 +00:00
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
func (h *Handler) Handler() http.Handler {
|
2022-01-13 20:49:41 +00:00
|
|
|
chain := middleware.Chain{
|
|
|
|
middleware.CSRFWithConfig(middleware.CSRFConfig{
|
2023-01-14 21:27:37 +00:00
|
|
|
Skipper: func(w http.ResponseWriter, r *http.Request) bool {
|
|
|
|
head, _ := urlutil.ShiftPath(r.URL.Path)
|
2022-02-07 20:34:03 +00:00
|
|
|
|
2023-01-16 18:42:07 +00:00
|
|
|
return head == ""
|
2022-01-13 20:49:41 +00:00
|
|
|
},
|
2022-02-01 17:27:48 +00:00
|
|
|
CookieMaxAge: 0,
|
2023-01-14 21:27:37 +00:00
|
|
|
CookieSameSite: http.SameSiteStrictMode,
|
2022-02-07 20:34:03 +00:00
|
|
|
ContextKey: "csrf",
|
|
|
|
CookieDomain: h.config.Server.Domain,
|
|
|
|
CookieName: "__Secure-csrf",
|
2022-06-22 14:22:19 +00:00
|
|
|
CookiePath: "/authorize",
|
2022-02-25 23:50:22 +00:00
|
|
|
TokenLookup: "param:_csrf",
|
2022-02-01 17:27:48 +00:00
|
|
|
TokenLength: 0,
|
|
|
|
CookieSecure: true,
|
|
|
|
CookieHTTPOnly: true,
|
2022-01-20 23:33:15 +00:00
|
|
|
}),
|
|
|
|
middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{
|
2023-01-14 21:27:37 +00:00
|
|
|
Skipper: func(w http.ResponseWriter, r *http.Request) bool {
|
|
|
|
head, _ := urlutil.ShiftPath(r.URL.Path)
|
2022-01-20 23:33:15 +00:00
|
|
|
|
2023-01-16 18:42:07 +00:00
|
|
|
return r.Method != http.MethodPost ||
|
|
|
|
head != "verify" ||
|
|
|
|
r.PostFormValue("authorize") == "deny"
|
2022-01-20 23:33:15 +00:00
|
|
|
},
|
2023-01-16 18:42:07 +00:00
|
|
|
Validator: func(_ http.ResponseWriter, _ *http.Request, login, password string) (bool, error) {
|
2022-02-01 17:27:48 +00:00
|
|
|
userMatch := subtle.ConstantTimeCompare([]byte(login),
|
|
|
|
[]byte(h.config.IndieAuth.Username))
|
|
|
|
passMatch := subtle.ConstantTimeCompare([]byte(password),
|
|
|
|
[]byte(h.config.IndieAuth.Password))
|
2022-01-31 16:17:19 +00:00
|
|
|
|
|
|
|
return userMatch == 1 && passMatch == 1, nil
|
2022-01-20 23:33:15 +00:00
|
|
|
},
|
2022-02-01 17:27:48 +00:00
|
|
|
Realm: "",
|
2022-01-13 20:49:41 +00:00
|
|
|
}),
|
|
|
|
}
|
2021-07-21 23:04:00 +00:00
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2023-01-16 18:42:07 +00:00
|
|
|
head, _ := urlutil.ShiftPath(r.URL.Path)
|
2023-01-14 21:27:37 +00:00
|
|
|
|
|
|
|
switch r.Method {
|
|
|
|
default:
|
|
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
|
|
case http.MethodGet, "":
|
|
|
|
if head != "" {
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
chain.Handler(h.handleAuthorize).ServeHTTP(w, r)
|
|
|
|
case http.MethodPost:
|
|
|
|
switch head {
|
|
|
|
default:
|
|
|
|
http.NotFound(w, r)
|
|
|
|
case "":
|
|
|
|
chain.Handler(h.handleExchange).ServeHTTP(w, r)
|
|
|
|
case "verify":
|
|
|
|
chain.Handler(h.handleVerify).ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2021-07-21 23:04:00 +00:00
|
|
|
}
|
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
func (h *Handler) handleAuthorize(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if r.Method != http.MethodGet && r.Method != "" {
|
|
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
2022-01-31 16:15:38 +00:00
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
|
|
|
|
|
|
|
|
tags, _, _ := language.ParseAcceptLanguage(r.Header.Get(common.HeaderAcceptLanguage))
|
2022-01-31 16:15:38 +00:00
|
|
|
tag, _, _ := h.matcher.Match(tags...)
|
|
|
|
baseOf := web.BaseOf{
|
2023-01-14 21:27:37 +00:00
|
|
|
Config: &h.config,
|
2022-01-31 16:15:38 +00:00
|
|
|
Language: tag,
|
|
|
|
Printer: message.NewPrinter(tag),
|
|
|
|
}
|
|
|
|
|
2022-02-17 16:10:52 +00:00
|
|
|
req := NewAuthAuthorizationRequest()
|
2023-01-14 21:27:37 +00:00
|
|
|
if err := req.bind(r); err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
web.WriteTemplate(w, &web.ErrorPage{
|
2022-01-31 16:15:38 +00:00
|
|
|
BaseOf: baseOf,
|
|
|
|
Error: err,
|
|
|
|
})
|
2021-07-21 23:04:00 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
client, err := h.clients.Discovery(r.Context(), req.ClientID)
|
2021-07-21 23:04:00 +00:00
|
|
|
if err != nil {
|
2023-01-14 21:27:37 +00:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
web.WriteTemplate(w, &web.ErrorPage{
|
2022-01-31 16:15:38 +00:00
|
|
|
BaseOf: baseOf,
|
|
|
|
Error: err,
|
|
|
|
})
|
2021-07-21 23:04:00 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-02 01:15:11 +00:00
|
|
|
if !client.ValidateRedirectURI(req.RedirectURI.URL) {
|
2023-01-14 21:27:37 +00:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
web.WriteTemplate(w, &web.ErrorPage{
|
2022-01-31 16:15:38 +00:00
|
|
|
BaseOf: baseOf,
|
|
|
|
Error: domain.NewError(
|
|
|
|
domain.ErrorCodeInvalidClient,
|
|
|
|
"requested redirect_uri is not registered on client_id side",
|
|
|
|
"",
|
|
|
|
),
|
|
|
|
})
|
2022-01-13 20:49:41 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
csrf, _ := r.Context().Value(middleware.DefaultCSRFConfig.ContextKey).([]byte)
|
|
|
|
web.WriteTemplate(w, &web.AuthorizePage{
|
2022-01-31 16:15:38 +00:00
|
|
|
BaseOf: baseOf,
|
2021-07-21 23:04:00 +00:00
|
|
|
CSRF: csrf,
|
2022-02-01 17:27:48 +00:00
|
|
|
Scope: req.Scope,
|
|
|
|
Client: client,
|
2023-01-14 21:27:37 +00:00
|
|
|
Me: &req.Me,
|
|
|
|
RedirectURI: &req.RedirectURI,
|
|
|
|
CodeChallengeMethod: *req.CodeChallengeMethod,
|
2022-01-13 20:49:41 +00:00
|
|
|
ResponseType: req.ResponseType,
|
2022-02-01 17:27:48 +00:00
|
|
|
CodeChallenge: req.CodeChallenge,
|
2022-01-13 20:49:41 +00:00
|
|
|
State: req.State,
|
2022-02-01 17:27:48 +00:00
|
|
|
Providers: make([]*domain.Provider, 0), // TODO(toby3d)
|
2021-07-21 23:04:00 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
func (h *Handler) handleVerify(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.HeaderAccessControlAllowOrigin, h.config.Server.Domain)
|
|
|
|
w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
|
2021-07-21 23:04:00 +00:00
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
encoder := json.NewEncoder(w)
|
2021-07-21 23:04:00 +00:00
|
|
|
|
2022-02-02 21:12:09 +00:00
|
|
|
req := NewAuthVerifyRequest()
|
2023-01-14 21:27:37 +00:00
|
|
|
if err := req.bind(r); err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2022-02-01 17:27:48 +00:00
|
|
|
|
|
|
|
_ = encoder.Encode(err)
|
2021-07-21 23:04:00 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-13 20:49:41 +00:00
|
|
|
if strings.EqualFold(req.Authorize, "deny") {
|
2022-01-29 19:31:52 +00:00
|
|
|
domain.NewError(domain.ErrorCodeAccessDenied, "user deny authorization request", "", req.State).
|
2023-01-02 01:15:11 +00:00
|
|
|
SetReirectURI(req.RedirectURI.URL)
|
2023-01-14 21:27:37 +00:00
|
|
|
http.Redirect(w, r, req.RedirectURI.String(), http.StatusFound)
|
2021-07-21 23:04:00 +00:00
|
|
|
|
2022-01-13 20:49:41 +00:00
|
|
|
return
|
2021-07-21 23:04:00 +00:00
|
|
|
}
|
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
code, err := h.useCase.Generate(r.Context(), auth.GenerateOptions{
|
2022-01-13 20:49:41 +00:00
|
|
|
ClientID: req.ClientID,
|
2022-02-17 16:10:52 +00:00
|
|
|
Me: req.Me,
|
2023-01-02 01:15:11 +00:00
|
|
|
RedirectURI: req.RedirectURI.URL,
|
2023-01-14 21:27:37 +00:00
|
|
|
CodeChallengeMethod: *req.CodeChallengeMethod,
|
2022-01-13 20:49:41 +00:00
|
|
|
Scope: req.Scope,
|
2022-02-17 16:10:52 +00:00
|
|
|
CodeChallenge: req.CodeChallenge,
|
2022-01-13 20:49:41 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
2023-01-14 21:27:37 +00:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
2022-02-01 17:27:48 +00:00
|
|
|
|
|
|
|
_ = encoder.Encode(err)
|
2021-07-21 23:04:00 +00:00
|
|
|
|
2022-01-13 20:49:41 +00:00
|
|
|
return
|
2021-07-21 23:04:00 +00:00
|
|
|
}
|
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
q := req.RedirectURI.Query()
|
|
|
|
|
2022-01-13 20:49:41 +00:00
|
|
|
for key, val := range map[string]string{
|
|
|
|
"code": code,
|
|
|
|
"iss": h.config.Server.GetRootURL(),
|
|
|
|
"state": req.State,
|
|
|
|
} {
|
2023-01-14 21:27:37 +00:00
|
|
|
q.Set(key, val)
|
2021-07-21 23:04:00 +00:00
|
|
|
}
|
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
req.RedirectURI.RawQuery = q.Encode()
|
|
|
|
|
|
|
|
http.Redirect(w, r, req.RedirectURI.String(), http.StatusFound)
|
2021-07-21 23:04:00 +00:00
|
|
|
}
|
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
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
|
|
|
|
}
|
2022-01-13 20:49:41 +00:00
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
|
|
|
|
|
|
|
|
encoder := json.NewEncoder(w)
|
2022-01-13 20:49:41 +00:00
|
|
|
|
2022-02-01 17:27:48 +00:00
|
|
|
req := new(AuthExchangeRequest)
|
2023-01-14 21:27:37 +00:00
|
|
|
if err := req.bind(r); err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2022-02-01 17:27:48 +00:00
|
|
|
|
|
|
|
_ = encoder.Encode(err)
|
2021-07-21 23:04:00 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-14 21:27:37 +00:00
|
|
|
me, profile, err := h.useCase.Exchange(r.Context(), auth.ExchangeOptions{
|
2022-01-13 20:49:41 +00:00
|
|
|
Code: req.Code,
|
|
|
|
ClientID: req.ClientID,
|
2023-01-02 01:15:11 +00:00
|
|
|
RedirectURI: req.RedirectURI.URL,
|
2022-01-13 20:49:41 +00:00
|
|
|
CodeVerifier: req.CodeVerifier,
|
|
|
|
})
|
2021-07-21 23:04:00 +00:00
|
|
|
if err != nil {
|
2023-01-14 21:27:37 +00:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2022-02-01 17:27:48 +00:00
|
|
|
|
|
|
|
_ = encoder.Encode(err)
|
2021-07-21 23:04:00 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-02-17 16:10:52 +00:00
|
|
|
var userInfo *AuthProfileResponse
|
|
|
|
if profile != nil {
|
|
|
|
userInfo = &AuthProfileResponse{
|
|
|
|
Email: profile.GetEmail(),
|
2023-01-02 01:15:11 +00:00
|
|
|
Photo: &domain.URL{URL: profile.GetPhoto()},
|
|
|
|
URL: &domain.URL{URL: profile.GetURL()},
|
2022-02-17 16:10:52 +00:00
|
|
|
Name: profile.GetName(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-01 17:27:48 +00:00
|
|
|
_ = encoder.Encode(&AuthExchangeResponse{
|
2023-01-14 21:27:37 +00:00
|
|
|
Me: *me,
|
2022-02-17 16:10:52 +00:00
|
|
|
Profile: userInfo,
|
2022-01-13 20:49:41 +00:00
|
|
|
})
|
|
|
|
}
|