Added profiles data support

This commit is contained in:
Maxim Lebedev 2022-02-17 21:10:52 +05:00
parent bdd633bc8d
commit 0dcfdfc1ac
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
5 changed files with 80 additions and 38 deletions

View File

@ -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) {

View File

@ -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,
}
}

View File

@ -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)
}
)

View File

@ -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
}

View File

@ -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
}