2023-01-14 21:27:37 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"source.toby3d.me/toby3d/auth/internal/domain"
|
2024-05-06 15:58:14 +00:00
|
|
|
"source.toby3d.me/toby3d/auth/internal/domain/challenge"
|
2024-05-06 15:45:19 +00:00
|
|
|
"source.toby3d.me/toby3d/auth/internal/domain/grant"
|
|
|
|
"source.toby3d.me/toby3d/auth/internal/domain/response"
|
2023-01-14 21:27:37 +00:00
|
|
|
"source.toby3d.me/toby3d/form"
|
|
|
|
)
|
|
|
|
|
|
|
|
type (
|
|
|
|
AuthAuthorizationRequest struct {
|
|
|
|
// The client URL.
|
|
|
|
ClientID domain.ClientID `form:"client_id"`
|
|
|
|
|
|
|
|
// The redirect URL indicating where the user should be
|
|
|
|
// redirected to after approving the request.
|
|
|
|
RedirectURI domain.URL `form:"redirect_uri"`
|
|
|
|
|
|
|
|
// The URL that the user entered.
|
|
|
|
Me domain.Me `form:"me"`
|
|
|
|
|
|
|
|
// The hashing method used to calculate the code challenge.
|
2024-05-06 15:58:14 +00:00
|
|
|
CodeChallengeMethod challenge.Method `form:"code_challenge_method,omitempty"`
|
2023-01-14 21:27:37 +00:00
|
|
|
|
2023-03-16 15:30:00 +00:00
|
|
|
// Indicates to the authorization server that an authorization
|
|
|
|
// code should be returned as the response.
|
2024-05-06 15:45:19 +00:00
|
|
|
ResponseType response.Type `form:"response_type"` // code
|
2023-01-14 21:27:37 +00:00
|
|
|
|
|
|
|
// A parameter set by the client which will be included when the
|
|
|
|
// user is redirected back to the client. This is used to
|
|
|
|
// prevent CSRF attacks. The authorization server MUST return
|
|
|
|
// the unmodified state value back to the client.
|
|
|
|
State string `form:"state"`
|
|
|
|
|
|
|
|
// The code challenge as previously described.
|
|
|
|
CodeChallenge string `form:"code_challenge,omitempty"`
|
2023-03-16 15:30:00 +00:00
|
|
|
|
|
|
|
// A space-separated list of scopes the client is requesting,
|
|
|
|
// e.g. "profile", or "profile create". If the client omits this
|
|
|
|
// value, the authorization server MUST NOT issue an access
|
|
|
|
// token for this authorization code. Only the user's profile
|
|
|
|
// URL may be returned without any scope requested.
|
|
|
|
Scope domain.Scopes `form:"scope,omitempty"`
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
AuthVerifyRequest struct {
|
2024-05-06 15:58:14 +00:00
|
|
|
ClientID domain.ClientID `form:"client_id"`
|
|
|
|
Me domain.Me `form:"me"`
|
|
|
|
RedirectURI domain.URL `form:"redirect_uri"`
|
|
|
|
CodeChallengeMethod challenge.Method `form:"code_challenge_method,omitempty"`
|
|
|
|
ResponseType response.Type `form:"response_type"`
|
|
|
|
Authorize string `form:"authorize"`
|
|
|
|
CodeChallenge string `form:"code_challenge,omitempty"`
|
|
|
|
State string `form:"state"`
|
|
|
|
Provider string `form:"provider"`
|
|
|
|
Scope domain.Scopes `form:"scope[],omitempty"`
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
AuthExchangeRequest struct {
|
2024-05-06 15:45:19 +00:00
|
|
|
GrantType grant.Type `form:"grant_type"` // authorization_code
|
2023-01-14 21:27:37 +00:00
|
|
|
|
|
|
|
// The client's URL, which MUST match the client_id used in the
|
|
|
|
// authentication request.
|
|
|
|
ClientID domain.ClientID `form:"client_id"`
|
|
|
|
|
|
|
|
// The client's redirect URL, which MUST match the initial
|
|
|
|
// authentication request.
|
|
|
|
RedirectURI domain.URL `form:"redirect_uri"`
|
|
|
|
|
|
|
|
// The authorization code received from the authorization
|
|
|
|
// endpoint in the redirect.
|
|
|
|
Code string `form:"code"`
|
|
|
|
|
|
|
|
// The original plaintext random string generated before
|
|
|
|
// starting the authorization request.
|
|
|
|
CodeVerifier string `form:"code_verifier"`
|
|
|
|
}
|
|
|
|
|
|
|
|
AuthExchangeResponse struct {
|
|
|
|
Profile *AuthProfileResponse `json:"profile,omitempty"`
|
2023-08-07 03:08:44 +00:00
|
|
|
Me string `json:"me"`
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
AuthProfileResponse struct {
|
2023-08-07 03:08:44 +00:00
|
|
|
Email string `json:"email,omitempty"`
|
|
|
|
Photo string `json:"photo,omitempty"`
|
|
|
|
URL string `json:"url,omitempty"`
|
|
|
|
Name string `json:"name,omitempty"`
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
func NewAuthAuthorizationRequest() *AuthAuthorizationRequest {
|
|
|
|
return &AuthAuthorizationRequest{
|
|
|
|
ClientID: domain.ClientID{},
|
|
|
|
CodeChallenge: "",
|
2024-05-06 15:58:14 +00:00
|
|
|
CodeChallengeMethod: challenge.Und,
|
2023-01-14 21:27:37 +00:00
|
|
|
Me: domain.Me{},
|
|
|
|
RedirectURI: domain.URL{},
|
2024-05-06 15:45:19 +00:00
|
|
|
ResponseType: response.Und,
|
2023-01-14 21:27:37 +00:00
|
|
|
Scope: make(domain.Scopes, 0),
|
|
|
|
State: "",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *AuthAuthorizationRequest) bind(req *http.Request) error {
|
|
|
|
indieAuthError := new(domain.Error)
|
|
|
|
|
|
|
|
if err := form.Unmarshal([]byte(req.URL.Query().Encode()), r); err != nil {
|
|
|
|
if errors.As(err, indieAuthError) {
|
|
|
|
return indieAuthError
|
|
|
|
}
|
|
|
|
|
2023-01-16 10:37:27 +00:00
|
|
|
return domain.NewError(domain.ErrorCodeInvalidRequest, err.Error(),
|
|
|
|
"https://indieauth.net/source/#authorization-request")
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
|
2024-05-06 15:45:19 +00:00
|
|
|
if r.ResponseType == response.ID {
|
|
|
|
r.ResponseType = response.Code
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewAuthVerifyRequest() *AuthVerifyRequest {
|
|
|
|
return &AuthVerifyRequest{
|
|
|
|
Authorize: "",
|
|
|
|
ClientID: domain.ClientID{},
|
|
|
|
CodeChallenge: "",
|
2024-05-06 15:58:14 +00:00
|
|
|
CodeChallengeMethod: challenge.Und,
|
2023-01-14 21:27:37 +00:00
|
|
|
Me: domain.Me{},
|
|
|
|
Provider: "",
|
|
|
|
RedirectURI: domain.URL{},
|
2024-05-06 15:45:19 +00:00
|
|
|
ResponseType: response.Und,
|
2023-01-14 21:27:37 +00:00
|
|
|
Scope: make(domain.Scopes, 0),
|
|
|
|
State: "",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *AuthVerifyRequest) bind(req *http.Request) error {
|
|
|
|
indieAuthError := new(domain.Error)
|
|
|
|
|
|
|
|
if err := req.ParseForm(); err != nil {
|
2023-01-16 10:37:27 +00:00
|
|
|
return domain.NewError(domain.ErrorCodeInvalidRequest, err.Error(),
|
|
|
|
"https://indieauth.net/source/#authorization-request")
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := form.Unmarshal([]byte(req.PostForm.Encode()), r); err != nil {
|
|
|
|
if errors.As(err, indieAuthError) {
|
|
|
|
return indieAuthError
|
|
|
|
}
|
|
|
|
|
2023-01-16 10:37:27 +00:00
|
|
|
return domain.NewError(domain.ErrorCodeInvalidRequest, err.Error(),
|
|
|
|
"https://indieauth.net/source/#authorization-request")
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE(toby3d): backwards-compatible support.
|
|
|
|
// See: https://aaronparecki.com/2020/12/03/1/indieauth-2020#response-type
|
2024-05-06 15:45:19 +00:00
|
|
|
if r.ResponseType == response.ID {
|
|
|
|
r.ResponseType = response.Code
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
r.Provider = strings.ToLower(r.Provider)
|
|
|
|
|
|
|
|
if !strings.EqualFold(r.Authorize, "allow") && !strings.EqualFold(r.Authorize, "deny") {
|
2023-01-16 10:37:27 +00:00
|
|
|
return domain.NewError(domain.ErrorCodeInvalidRequest, "cannot validate verification request",
|
|
|
|
"https://indieauth.net/source/#authorization-request")
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *AuthExchangeRequest) bind(req *http.Request) error {
|
|
|
|
indieAuthError := new(domain.Error)
|
|
|
|
|
|
|
|
if err := req.ParseForm(); err != nil {
|
2023-01-16 10:37:27 +00:00
|
|
|
return domain.NewError(domain.ErrorCodeInvalidRequest, err.Error(),
|
|
|
|
"https://indieauth.net/source/#authorization-request")
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := form.Unmarshal([]byte(req.PostForm.Encode()), r); err != nil {
|
|
|
|
if errors.As(err, indieAuthError) {
|
|
|
|
return indieAuthError
|
|
|
|
}
|
|
|
|
|
2023-01-16 10:37:27 +00:00
|
|
|
return domain.NewError(domain.ErrorCodeInvalidRequest, "cannot validate verification request",
|
|
|
|
"https://indieauth.net/source/#redeeming-the-authorization-code")
|
2023-01-14 21:27:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2023-08-07 03:08:44 +00:00
|
|
|
|
|
|
|
func NewAuthProfileResponse(in *domain.Profile) *AuthProfileResponse {
|
|
|
|
out := new(AuthProfileResponse)
|
|
|
|
|
|
|
|
if in == nil {
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
out.Name = in.Name
|
|
|
|
|
|
|
|
if in.URL != nil {
|
|
|
|
out.URL = in.URL.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
if in.Email != nil {
|
|
|
|
out.Email = in.Email.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
if in.Photo != nil {
|
|
|
|
out.Photo = in.Photo.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
|
|
|
}
|