✨ 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/client"
|
||||||
"source.toby3d.me/website/indieauth/internal/common"
|
"source.toby3d.me/website/indieauth/internal/common"
|
||||||
"source.toby3d.me/website/indieauth/internal/domain"
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/profile"
|
||||||
"source.toby3d.me/website/indieauth/web"
|
"source.toby3d.me/website/indieauth/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
AuthAuthorizeRequest struct {
|
AuthAuthorizationRequest struct {
|
||||||
// Indicates to the authorization server that an authorization
|
// Indicates to the authorization server that an authorization
|
||||||
// code should be returned as the response.
|
// code should be returned as the response.
|
||||||
ResponseType domain.ResponseType `form:"response_type"` // code
|
ResponseType domain.ResponseType `form:"response_type"` // code
|
||||||
|
@ -92,14 +93,23 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthExchangeResponse struct {
|
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 {
|
NewRequestHandlerOptions struct {
|
||||||
Auth auth.UseCase
|
Auth auth.UseCase
|
||||||
Clients client.UseCase
|
Clients client.UseCase
|
||||||
Config *domain.Config
|
Config *domain.Config
|
||||||
Matcher language.Matcher
|
Matcher language.Matcher
|
||||||
|
Profiles profile.UseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestHandler struct {
|
RequestHandler struct {
|
||||||
|
@ -175,7 +185,7 @@ func (h *RequestHandler) handleAuthorize(ctx *http.RequestCtx) {
|
||||||
Printer: message.NewPrinter(tag),
|
Printer: message.NewPrinter(tag),
|
||||||
}
|
}
|
||||||
|
|
||||||
req := NewAuthAuthorizeRequest()
|
req := NewAuthAuthorizationRequest()
|
||||||
if err := req.bind(ctx); err != nil {
|
if err := req.bind(ctx); err != nil {
|
||||||
ctx.SetStatusCode(http.StatusBadRequest)
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
web.WriteTemplate(ctx, &web.ErrorPage{
|
web.WriteTemplate(ctx, &web.ErrorPage{
|
||||||
|
@ -256,11 +266,11 @@ func (h *RequestHandler) handleVerify(ctx *http.RequestCtx) {
|
||||||
|
|
||||||
code, err := h.useCase.Generate(ctx, auth.GenerateOptions{
|
code, err := h.useCase.Generate(ctx, auth.GenerateOptions{
|
||||||
ClientID: req.ClientID,
|
ClientID: req.ClientID,
|
||||||
|
Me: req.Me,
|
||||||
RedirectURI: req.RedirectURI,
|
RedirectURI: req.RedirectURI,
|
||||||
CodeChallenge: req.CodeChallenge,
|
|
||||||
CodeChallengeMethod: req.CodeChallengeMethod,
|
CodeChallengeMethod: req.CodeChallengeMethod,
|
||||||
Scope: req.Scope,
|
Scope: req.Scope,
|
||||||
Me: req.Me,
|
CodeChallenge: req.CodeChallenge,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SetStatusCode(http.StatusInternalServerError)
|
ctx.SetStatusCode(http.StatusInternalServerError)
|
||||||
|
@ -295,7 +305,7 @@ func (h *RequestHandler) handleExchange(ctx *http.RequestCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
me, err := h.useCase.Exchange(ctx, auth.ExchangeOptions{
|
me, profile, err := h.useCase.Exchange(ctx, auth.ExchangeOptions{
|
||||||
Code: req.Code,
|
Code: req.Code,
|
||||||
ClientID: req.ClientID,
|
ClientID: req.ClientID,
|
||||||
RedirectURI: req.RedirectURI,
|
RedirectURI: req.RedirectURI,
|
||||||
|
@ -309,13 +319,24 @@ func (h *RequestHandler) handleExchange(ctx *http.RequestCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userInfo *AuthProfileResponse
|
||||||
|
if profile != nil {
|
||||||
|
userInfo = &AuthProfileResponse{
|
||||||
|
Email: profile.GetEmail(),
|
||||||
|
Photo: profile.GetPhoto(),
|
||||||
|
URL: profile.GetURL(),
|
||||||
|
Name: profile.GetName(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = encoder.Encode(&AuthExchangeResponse{
|
_ = encoder.Encode(&AuthExchangeResponse{
|
||||||
Me: me,
|
Me: me,
|
||||||
|
Profile: userInfo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthAuthorizeRequest() *AuthAuthorizeRequest {
|
func NewAuthAuthorizationRequest() *AuthAuthorizationRequest {
|
||||||
return &AuthAuthorizeRequest{
|
return &AuthAuthorizationRequest{
|
||||||
ClientID: new(domain.ClientID),
|
ClientID: new(domain.ClientID),
|
||||||
CodeChallenge: "",
|
CodeChallenge: "",
|
||||||
CodeChallengeMethod: domain.CodeChallengeMethodUndefined,
|
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)
|
indieAuthError := new(domain.Error)
|
||||||
if err := form.Unmarshal(ctx.QueryArgs(), r); err != nil {
|
if err := form.Unmarshal(ctx.QueryArgs(), r); err != nil {
|
||||||
if errors.As(err, indieAuthError) {
|
if errors.As(err, indieAuthError) {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
clientrepo "source.toby3d.me/website/indieauth/internal/client/repository/memory"
|
clientrepo "source.toby3d.me/website/indieauth/internal/client/repository/memory"
|
||||||
clientucase "source.toby3d.me/website/indieauth/internal/client/usecase"
|
clientucase "source.toby3d.me/website/indieauth/internal/client/usecase"
|
||||||
"source.toby3d.me/website/indieauth/internal/domain"
|
"source.toby3d.me/website/indieauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/indieauth/internal/profile"
|
||||||
profilerepo "source.toby3d.me/website/indieauth/internal/profile/repository/memory"
|
profilerepo "source.toby3d.me/website/indieauth/internal/profile/repository/memory"
|
||||||
"source.toby3d.me/website/indieauth/internal/session"
|
"source.toby3d.me/website/indieauth/internal/session"
|
||||||
sessionrepo "source.toby3d.me/website/indieauth/internal/session/repository/memory"
|
sessionrepo "source.toby3d.me/website/indieauth/internal/session/repository/memory"
|
||||||
|
@ -31,6 +32,7 @@ type Dependencies struct {
|
||||||
clientService client.UseCase
|
clientService client.UseCase
|
||||||
config *domain.Config
|
config *domain.Config
|
||||||
matcher language.Matcher
|
matcher language.Matcher
|
||||||
|
profiles profile.Repository
|
||||||
sessions session.Repository
|
sessions session.Repository
|
||||||
store *sync.Map
|
store *sync.Map
|
||||||
}
|
}
|
||||||
|
@ -103,7 +105,8 @@ func NewDependencies(tb testing.TB) Dependencies {
|
||||||
store := new(sync.Map)
|
store := new(sync.Map)
|
||||||
clients := clientrepo.NewMemoryClientRepository(store)
|
clients := clientrepo.NewMemoryClientRepository(store)
|
||||||
sessions := sessionrepo.NewMemorySessionRepository(store, config)
|
sessions := sessionrepo.NewMemorySessionRepository(store, config)
|
||||||
authService := ucase.NewAuthUseCase(sessions, config)
|
profiles := profilerepo.NewMemoryProfileRepository(store)
|
||||||
|
authService := ucase.NewAuthUseCase(sessions, profiles, config)
|
||||||
clientService := clientucase.NewClientUseCase(clients)
|
clientService := clientucase.NewClientUseCase(clients)
|
||||||
|
|
||||||
return Dependencies{
|
return Dependencies{
|
||||||
|
@ -113,6 +116,7 @@ func NewDependencies(tb testing.TB) Dependencies {
|
||||||
config: config,
|
config: config,
|
||||||
matcher: matcher,
|
matcher: matcher,
|
||||||
sessions: sessions,
|
sessions: sessions,
|
||||||
|
profiles: profiles,
|
||||||
store: store,
|
store: store,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ type (
|
||||||
|
|
||||||
UseCase interface {
|
UseCase interface {
|
||||||
Generate(ctx context.Context, opts GenerateOptions) (string, error)
|
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/auth"
|
||||||
"source.toby3d.me/website/indieauth/internal/domain"
|
"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/random"
|
||||||
"source.toby3d.me/website/indieauth/internal/session"
|
"source.toby3d.me/website/indieauth/internal/session"
|
||||||
)
|
)
|
||||||
|
@ -13,28 +14,48 @@ import (
|
||||||
type authUseCase struct {
|
type authUseCase struct {
|
||||||
config *domain.Config
|
config *domain.Config
|
||||||
sessions session.Repository
|
sessions session.Repository
|
||||||
|
profiles profile.Repository
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthUseCase creates a new authentication use case.
|
// 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{
|
return &authUseCase{
|
||||||
config: config,
|
config: config,
|
||||||
sessions: sessions,
|
sessions: sessions,
|
||||||
|
profiles: profiles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (useCase *authUseCase) Generate(ctx context.Context, opts auth.GenerateOptions) (string, error) {
|
func (uc *authUseCase) Generate(ctx context.Context, opts auth.GenerateOptions) (string, error) {
|
||||||
code, err := random.String(useCase.config.Code.Length)
|
code, err := random.String(uc.config.Code.Length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("cannot generate random code: %w", err)
|
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,
|
ClientID: opts.ClientID,
|
||||||
Code: code,
|
Code: code,
|
||||||
CodeChallenge: opts.CodeChallenge,
|
CodeChallenge: opts.CodeChallenge,
|
||||||
CodeChallengeMethod: opts.CodeChallengeMethod,
|
CodeChallengeMethod: opts.CodeChallengeMethod,
|
||||||
Me: opts.Me,
|
Me: opts.Me,
|
||||||
|
Profile: userInfo,
|
||||||
RedirectURI: opts.RedirectURI,
|
RedirectURI: opts.RedirectURI,
|
||||||
Scope: opts.Scope,
|
Scope: opts.Scope,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
@ -44,24 +65,25 @@ func (useCase *authUseCase) Generate(ctx context.Context, opts auth.GenerateOpti
|
||||||
return code, nil
|
return code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (useCase *authUseCase) Exchange(ctx context.Context, opts auth.ExchangeOptions) (*domain.Me, error) {
|
func (uc *authUseCase) Exchange(ctx context.Context, opts auth.ExchangeOptions) (*domain.Me, *domain.Profile, error) {
|
||||||
session, err := useCase.sessions.GetAndDelete(ctx, opts.Code)
|
session, err := uc.sessions.GetAndDelete(ctx, opts.Code)
|
||||||
if err != nil {
|
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() {
|
if opts.ClientID.String() != session.ClientID.String() {
|
||||||
return nil, auth.ErrMismatchClientID
|
return nil, nil, auth.ErrMismatchClientID
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.RedirectURI.String() != session.RedirectURI.String() {
|
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) {
|
!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,
|
Me: resolvedMe,
|
||||||
Micropub: nil,
|
Micropub: nil,
|
||||||
Microsub: nil,
|
Microsub: nil,
|
||||||
Profile: &domain.Profile{
|
Profile: domain.NewProfile(),
|
||||||
Email: make([]*domain.Email, 0),
|
TicketEndpoint: nil,
|
||||||
Name: make([]string, 0),
|
TokenEndpoint: nil,
|
||||||
Photo: make([]*domain.URL, 0),
|
|
||||||
URL: make([]*domain.URL, 0),
|
|
||||||
},
|
|
||||||
TicketEndpoint: nil,
|
|
||||||
TokenEndpoint: nil,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata, err := util.ExtractMetadata(resp, repo.client); err == nil {
|
if metadata, err := util.ExtractMetadata(resp, repo.client); err == nil {
|
||||||
user.AuthorizationEndpoint = metadata.AuthorizationEndpoint
|
user.AuthorizationEndpoint = metadata.AuthorizationEndpoint
|
||||||
user.Micropub = metadata.Micropub
|
user.Micropub = metadata.MicropubEndpoint
|
||||||
user.Microsub = metadata.Microsub
|
user.Microsub = metadata.MicrosubEndpoint
|
||||||
user.TicketEndpoint = metadata.TicketEndpoint
|
user.TicketEndpoint = metadata.TicketEndpoint
|
||||||
user.TokenEndpoint = metadata.TokenEndpoint
|
user.TokenEndpoint = metadata.TokenEndpoint
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue