✨ Added profiles data support
This commit is contained in:
parent
bdd633bc8d
commit
0dcfdfc1ac
|
@ -19,11 +19,12 @@ import (
|
|||
"source.toby3d.me/website/indieauth/internal/client"
|
||||
"source.toby3d.me/website/indieauth/internal/common"
|
||||
"source.toby3d.me/website/indieauth/internal/domain"
|
||||
"source.toby3d.me/website/indieauth/internal/profile"
|
||||
"source.toby3d.me/website/indieauth/web"
|
||||
)
|
||||
|
||||
type (
|
||||
AuthAuthorizeRequest struct {
|
||||
AuthAuthorizationRequest struct {
|
||||
// Indicates to the authorization server that an authorization
|
||||
// code should be returned as the response.
|
||||
ResponseType domain.ResponseType `form:"response_type"` // code
|
||||
|
@ -92,14 +93,23 @@ type (
|
|||
}
|
||||
|
||||
AuthExchangeResponse struct {
|
||||
Me *domain.Me `json:"me"`
|
||||
Me *domain.Me `json:"me"`
|
||||
Profile *AuthProfileResponse `json:"profile,omitempty"`
|
||||
}
|
||||
|
||||
AuthProfileResponse struct {
|
||||
Email *domain.Email `json:"email,omitempty"`
|
||||
Photo *domain.URL `json:"photo,omitempty"`
|
||||
URL *domain.URL `json:"url,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
NewRequestHandlerOptions struct {
|
||||
Auth auth.UseCase
|
||||
Clients client.UseCase
|
||||
Config *domain.Config
|
||||
Matcher language.Matcher
|
||||
Auth auth.UseCase
|
||||
Clients client.UseCase
|
||||
Config *domain.Config
|
||||
Matcher language.Matcher
|
||||
Profiles profile.UseCase
|
||||
}
|
||||
|
||||
RequestHandler struct {
|
||||
|
@ -175,7 +185,7 @@ func (h *RequestHandler) handleAuthorize(ctx *http.RequestCtx) {
|
|||
Printer: message.NewPrinter(tag),
|
||||
}
|
||||
|
||||
req := NewAuthAuthorizeRequest()
|
||||
req := NewAuthAuthorizationRequest()
|
||||
if err := req.bind(ctx); err != nil {
|
||||
ctx.SetStatusCode(http.StatusBadRequest)
|
||||
web.WriteTemplate(ctx, &web.ErrorPage{
|
||||
|
@ -256,11 +266,11 @@ func (h *RequestHandler) handleVerify(ctx *http.RequestCtx) {
|
|||
|
||||
code, err := h.useCase.Generate(ctx, auth.GenerateOptions{
|
||||
ClientID: req.ClientID,
|
||||
Me: req.Me,
|
||||
RedirectURI: req.RedirectURI,
|
||||
CodeChallenge: req.CodeChallenge,
|
||||
CodeChallengeMethod: req.CodeChallengeMethod,
|
||||
Scope: req.Scope,
|
||||
Me: req.Me,
|
||||
CodeChallenge: req.CodeChallenge,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.SetStatusCode(http.StatusInternalServerError)
|
||||
|
@ -295,7 +305,7 @@ func (h *RequestHandler) handleExchange(ctx *http.RequestCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
me, err := h.useCase.Exchange(ctx, auth.ExchangeOptions{
|
||||
me, profile, err := h.useCase.Exchange(ctx, auth.ExchangeOptions{
|
||||
Code: req.Code,
|
||||
ClientID: req.ClientID,
|
||||
RedirectURI: req.RedirectURI,
|
||||
|
@ -309,13 +319,24 @@ func (h *RequestHandler) handleExchange(ctx *http.RequestCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
var userInfo *AuthProfileResponse
|
||||
if profile != nil {
|
||||
userInfo = &AuthProfileResponse{
|
||||
Email: profile.GetEmail(),
|
||||
Photo: profile.GetPhoto(),
|
||||
URL: profile.GetURL(),
|
||||
Name: profile.GetName(),
|
||||
}
|
||||
}
|
||||
|
||||
_ = encoder.Encode(&AuthExchangeResponse{
|
||||
Me: me,
|
||||
Me: me,
|
||||
Profile: userInfo,
|
||||
})
|
||||
}
|
||||
|
||||
func NewAuthAuthorizeRequest() *AuthAuthorizeRequest {
|
||||
return &AuthAuthorizeRequest{
|
||||
func NewAuthAuthorizationRequest() *AuthAuthorizationRequest {
|
||||
return &AuthAuthorizationRequest{
|
||||
ClientID: new(domain.ClientID),
|
||||
CodeChallenge: "",
|
||||
CodeChallengeMethod: domain.CodeChallengeMethodUndefined,
|
||||
|
@ -327,7 +348,7 @@ func NewAuthAuthorizeRequest() *AuthAuthorizeRequest {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *AuthAuthorizeRequest) bind(ctx *http.RequestCtx) error {
|
||||
func (r *AuthAuthorizationRequest) bind(ctx *http.RequestCtx) error {
|
||||
indieAuthError := new(domain.Error)
|
||||
if err := form.Unmarshal(ctx.QueryArgs(), r); err != nil {
|
||||
if errors.As(err, indieAuthError) {
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
clientrepo "source.toby3d.me/website/indieauth/internal/client/repository/memory"
|
||||
clientucase "source.toby3d.me/website/indieauth/internal/client/usecase"
|
||||
"source.toby3d.me/website/indieauth/internal/domain"
|
||||
"source.toby3d.me/website/indieauth/internal/profile"
|
||||
profilerepo "source.toby3d.me/website/indieauth/internal/profile/repository/memory"
|
||||
"source.toby3d.me/website/indieauth/internal/session"
|
||||
sessionrepo "source.toby3d.me/website/indieauth/internal/session/repository/memory"
|
||||
|
@ -31,6 +32,7 @@ type Dependencies struct {
|
|||
clientService client.UseCase
|
||||
config *domain.Config
|
||||
matcher language.Matcher
|
||||
profiles profile.Repository
|
||||
sessions session.Repository
|
||||
store *sync.Map
|
||||
}
|
||||
|
@ -103,7 +105,8 @@ func NewDependencies(tb testing.TB) Dependencies {
|
|||
store := new(sync.Map)
|
||||
clients := clientrepo.NewMemoryClientRepository(store)
|
||||
sessions := sessionrepo.NewMemorySessionRepository(store, config)
|
||||
authService := ucase.NewAuthUseCase(sessions, config)
|
||||
profiles := profilerepo.NewMemoryProfileRepository(store)
|
||||
authService := ucase.NewAuthUseCase(sessions, profiles, config)
|
||||
clientService := clientucase.NewClientUseCase(clients)
|
||||
|
||||
return Dependencies{
|
||||
|
@ -113,6 +116,7 @@ func NewDependencies(tb testing.TB) Dependencies {
|
|||
config: config,
|
||||
matcher: matcher,
|
||||
sessions: sessions,
|
||||
profiles: profiles,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ type (
|
|||
|
||||
UseCase interface {
|
||||
Generate(ctx context.Context, opts GenerateOptions) (string, error)
|
||||
Exchange(ctx context.Context, opts ExchangeOptions) (*domain.Me, error)
|
||||
Exchange(ctx context.Context, opts ExchangeOptions) (*domain.Me, *domain.Profile, error)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"source.toby3d.me/website/indieauth/internal/auth"
|
||||
"source.toby3d.me/website/indieauth/internal/domain"
|
||||
"source.toby3d.me/website/indieauth/internal/profile"
|
||||
"source.toby3d.me/website/indieauth/internal/random"
|
||||
"source.toby3d.me/website/indieauth/internal/session"
|
||||
)
|
||||
|
@ -13,28 +14,48 @@ import (
|
|||
type authUseCase struct {
|
||||
config *domain.Config
|
||||
sessions session.Repository
|
||||
profiles profile.Repository
|
||||
}
|
||||
|
||||
// NewAuthUseCase creates a new authentication use case.
|
||||
func NewAuthUseCase(sessions session.Repository, config *domain.Config) auth.UseCase {
|
||||
func NewAuthUseCase(sessions session.Repository, profiles profile.Repository, config *domain.Config) auth.UseCase {
|
||||
return &authUseCase{
|
||||
config: config,
|
||||
sessions: sessions,
|
||||
profiles: profiles,
|
||||
}
|
||||
}
|
||||
|
||||
func (useCase *authUseCase) Generate(ctx context.Context, opts auth.GenerateOptions) (string, error) {
|
||||
code, err := random.String(useCase.config.Code.Length)
|
||||
func (uc *authUseCase) Generate(ctx context.Context, opts auth.GenerateOptions) (string, error) {
|
||||
code, err := random.String(uc.config.Code.Length)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot generate random code: %w", err)
|
||||
}
|
||||
|
||||
if err = useCase.sessions.Create(ctx, &domain.Session{
|
||||
var userInfo *domain.Profile
|
||||
|
||||
// NOTE(toby3d): We request information about the profile only if there
|
||||
// is a corresponding Scope. However, the availability of this
|
||||
// information in the token is not guaranteed and is completely optional:
|
||||
// https://indieauth.net/source/#profile-information
|
||||
if opts.Scope.Has(domain.ScopeProfile) {
|
||||
userInfo, _ = uc.profiles.Get(ctx, opts.Me)
|
||||
|
||||
// NOTE(toby3d): 'email' Scope depends on 'profile'
|
||||
// Scope. Hide the email field if this information has
|
||||
// not been requested.
|
||||
if userInfo != nil && userInfo.Email != nil && !opts.Scope.Has(domain.ScopeEmail) {
|
||||
userInfo.Email = nil
|
||||
}
|
||||
}
|
||||
|
||||
if err = uc.sessions.Create(ctx, &domain.Session{
|
||||
ClientID: opts.ClientID,
|
||||
Code: code,
|
||||
CodeChallenge: opts.CodeChallenge,
|
||||
CodeChallengeMethod: opts.CodeChallengeMethod,
|
||||
Me: opts.Me,
|
||||
Profile: userInfo,
|
||||
RedirectURI: opts.RedirectURI,
|
||||
Scope: opts.Scope,
|
||||
}); err != nil {
|
||||
|
@ -44,24 +65,25 @@ func (useCase *authUseCase) Generate(ctx context.Context, opts auth.GenerateOpti
|
|||
return code, nil
|
||||
}
|
||||
|
||||
func (useCase *authUseCase) Exchange(ctx context.Context, opts auth.ExchangeOptions) (*domain.Me, error) {
|
||||
session, err := useCase.sessions.GetAndDelete(ctx, opts.Code)
|
||||
func (uc *authUseCase) Exchange(ctx context.Context, opts auth.ExchangeOptions) (*domain.Me, *domain.Profile, error) {
|
||||
session, err := uc.sessions.GetAndDelete(ctx, opts.Code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot find session in store: %w", err)
|
||||
return nil, nil, fmt.Errorf("cannot find session in store: %w", err)
|
||||
}
|
||||
|
||||
if opts.ClientID.String() != session.ClientID.String() {
|
||||
return nil, auth.ErrMismatchClientID
|
||||
return nil, nil, auth.ErrMismatchClientID
|
||||
}
|
||||
|
||||
if opts.RedirectURI.String() != session.RedirectURI.String() {
|
||||
return nil, auth.ErrMismatchRedirectURI
|
||||
return nil, nil, auth.ErrMismatchRedirectURI
|
||||
}
|
||||
|
||||
if session.CodeChallenge != "" && session.CodeChallengeMethod != domain.CodeChallengeMethodUndefined &&
|
||||
if session.CodeChallenge != "" &&
|
||||
session.CodeChallengeMethod != domain.CodeChallengeMethodUndefined &&
|
||||
!session.CodeChallengeMethod.Validate(session.CodeChallenge, opts.CodeVerifier) {
|
||||
return nil, auth.ErrMismatchPKCE
|
||||
return nil, nil, auth.ErrMismatchPKCE
|
||||
}
|
||||
|
||||
return session.Me, nil
|
||||
return session.Me, session.Profile, nil
|
||||
}
|
||||
|
|
|
@ -59,20 +59,15 @@ func (repo *httpUserRepository) Get(ctx context.Context, me *domain.Me) (*domain
|
|||
Me: resolvedMe,
|
||||
Micropub: nil,
|
||||
Microsub: nil,
|
||||
Profile: &domain.Profile{
|
||||
Email: make([]*domain.Email, 0),
|
||||
Name: make([]string, 0),
|
||||
Photo: make([]*domain.URL, 0),
|
||||
URL: make([]*domain.URL, 0),
|
||||
},
|
||||
TicketEndpoint: nil,
|
||||
TokenEndpoint: nil,
|
||||
Profile: domain.NewProfile(),
|
||||
TicketEndpoint: nil,
|
||||
TokenEndpoint: nil,
|
||||
}
|
||||
|
||||
if metadata, err := util.ExtractMetadata(resp, repo.client); err == nil {
|
||||
user.AuthorizationEndpoint = metadata.AuthorizationEndpoint
|
||||
user.Micropub = metadata.Micropub
|
||||
user.Microsub = metadata.Microsub
|
||||
user.Micropub = metadata.MicropubEndpoint
|
||||
user.Microsub = metadata.MicrosubEndpoint
|
||||
user.TicketEndpoint = metadata.TicketEndpoint
|
||||
user.TokenEndpoint = metadata.TokenEndpoint
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue