165 lines
4.6 KiB
Go
165 lines
4.6 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/supabase/auth/internal/crypto"
|
|
"github.com/supabase/auth/internal/models"
|
|
"github.com/supabase/auth/internal/storage"
|
|
)
|
|
|
|
// MagicLinkParams holds the parameters for a magic link request
|
|
type MagicLinkParams struct {
|
|
Email string `json:"email"`
|
|
Data map[string]interface{} `json:"data"`
|
|
CodeChallengeMethod string `json:"code_challenge_method"`
|
|
CodeChallenge string `json:"code_challenge"`
|
|
}
|
|
|
|
func (p *MagicLinkParams) Validate(a *API) error {
|
|
if p.Email == "" {
|
|
return unprocessableEntityError(ErrorCodeValidationFailed, "Password recovery requires an email")
|
|
}
|
|
var err error
|
|
p.Email, err = a.validateEmail(p.Email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := validatePKCEParams(p.CodeChallengeMethod, p.CodeChallenge); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MagicLink sends a recovery email
|
|
func (a *API) MagicLink(w http.ResponseWriter, r *http.Request) error {
|
|
ctx := r.Context()
|
|
db := a.db.WithContext(ctx)
|
|
config := a.config
|
|
|
|
if !config.External.Email.Enabled {
|
|
return unprocessableEntityError(ErrorCodeEmailProviderDisabled, "Email logins are disabled")
|
|
}
|
|
|
|
if !config.External.Email.MagicLinkEnabled {
|
|
return unprocessableEntityError(ErrorCodeEmailProviderDisabled, "Login with magic link is disabled")
|
|
}
|
|
|
|
params := &MagicLinkParams{}
|
|
jsonDecoder := json.NewDecoder(r.Body)
|
|
err := jsonDecoder.Decode(params)
|
|
if err != nil {
|
|
return badRequestError(ErrorCodeBadJSON, "Could not read verification params: %v", err).WithInternalError(err)
|
|
}
|
|
|
|
if err := params.Validate(a); err != nil {
|
|
return err
|
|
}
|
|
|
|
if params.Data == nil {
|
|
params.Data = make(map[string]interface{})
|
|
}
|
|
|
|
flowType := getFlowFromChallenge(params.CodeChallenge)
|
|
|
|
var isNewUser bool
|
|
aud := a.requestAud(ctx, r)
|
|
user, err := models.FindUserByEmailAndAudience(db, params.Email, aud)
|
|
if err != nil {
|
|
if models.IsNotFoundError(err) {
|
|
isNewUser = true
|
|
} else {
|
|
return internalServerError("Database error finding user").WithInternalError(err)
|
|
}
|
|
}
|
|
if user != nil {
|
|
isNewUser = !user.IsConfirmed()
|
|
}
|
|
if isNewUser {
|
|
// User either doesn't exist or hasn't completed the signup process.
|
|
// Sign them up with temporary password.
|
|
password := crypto.GeneratePassword(config.Password.RequiredCharacters, 33)
|
|
|
|
signUpParams := &SignupParams{
|
|
Email: params.Email,
|
|
Password: password,
|
|
Data: params.Data,
|
|
CodeChallengeMethod: params.CodeChallengeMethod,
|
|
CodeChallenge: params.CodeChallenge,
|
|
}
|
|
newBodyContent, err := json.Marshal(signUpParams)
|
|
if err != nil {
|
|
// SignupParams must always be marshallable
|
|
panic(fmt.Errorf("failed to marshal SignupParams: %w", err))
|
|
}
|
|
r.Body = io.NopCloser(strings.NewReader(string(newBodyContent)))
|
|
r.ContentLength = int64(len(string(newBodyContent)))
|
|
|
|
fakeResponse := &responseStub{}
|
|
if config.Mailer.Autoconfirm {
|
|
// signups are autoconfirmed, send magic link after signup
|
|
if err := a.Signup(fakeResponse, r); err != nil {
|
|
return err
|
|
}
|
|
newBodyContent := &SignupParams{
|
|
Email: params.Email,
|
|
Data: params.Data,
|
|
CodeChallengeMethod: params.CodeChallengeMethod,
|
|
CodeChallenge: params.CodeChallenge,
|
|
}
|
|
metadata, err := json.Marshal(newBodyContent)
|
|
if err != nil {
|
|
// SignupParams must always be marshallable
|
|
panic(fmt.Errorf("failed to marshal SignupParams: %w", err))
|
|
}
|
|
r.Body = io.NopCloser(bytes.NewReader(metadata))
|
|
return a.MagicLink(w, r)
|
|
}
|
|
// otherwise confirmation email already contains 'magic link'
|
|
if err := a.Signup(fakeResponse, r); err != nil {
|
|
return err
|
|
}
|
|
|
|
return sendJSON(w, http.StatusOK, make(map[string]string))
|
|
}
|
|
|
|
if isPKCEFlow(flowType) {
|
|
if _, err = generateFlowState(a.db, models.MagicLink.String(), models.MagicLink, params.CodeChallengeMethod, params.CodeChallenge, &user.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = db.Transaction(func(tx *storage.Connection) error {
|
|
if terr := models.NewAuditLogEntry(r, tx, user, models.UserRecoveryRequestedAction, "", nil); terr != nil {
|
|
return terr
|
|
}
|
|
return a.sendMagicLink(r, tx, user, flowType)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return sendJSON(w, http.StatusOK, make(map[string]string))
|
|
}
|
|
|
|
// responseStub only implement http responsewriter for ignoring
|
|
// incoming data from methods where it passed
|
|
type responseStub struct {
|
|
}
|
|
|
|
func (rw *responseStub) Header() http.Header {
|
|
return http.Header{}
|
|
}
|
|
|
|
func (rw *responseStub) Write(data []byte) (int, error) {
|
|
return 1, nil
|
|
}
|
|
|
|
func (rw *responseStub) WriteHeader(statusCode int) {
|
|
}
|