chatdesk-ui/auth_v2.169.0/internal/api/magic_link.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) {
}