599 lines
18 KiB
Go
599 lines
18 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
dbent "github.com/Wei-Shaw/sub2api/ent"
|
|
"github.com/Wei-Shaw/sub2api/ent/authidentity"
|
|
"github.com/Wei-Shaw/sub2api/ent/identityadoptiondecision"
|
|
dbuser "github.com/Wei-Shaw/sub2api/ent/user"
|
|
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const (
|
|
oauthPendingBrowserCookiePath = "/api/v1/auth/oauth"
|
|
oauthPendingBrowserCookieName = "oauth_pending_browser_session"
|
|
oauthPendingSessionCookiePath = "/api/v1/auth/oauth/pending"
|
|
oauthPendingSessionCookieName = "oauth_pending_session"
|
|
oauthPendingCookieMaxAgeSec = 10 * 60
|
|
|
|
oauthCompletionResponseKey = "completion_response"
|
|
)
|
|
|
|
type oauthPendingSessionPayload struct {
|
|
Intent string
|
|
Identity service.PendingAuthIdentityKey
|
|
TargetUserID *int64
|
|
ResolvedEmail string
|
|
RedirectTo string
|
|
BrowserSessionKey string
|
|
UpstreamIdentityClaims map[string]any
|
|
CompletionResponse map[string]any
|
|
}
|
|
|
|
type oauthAdoptionDecisionRequest struct {
|
|
AdoptDisplayName *bool `json:"adopt_display_name,omitempty"`
|
|
AdoptAvatar *bool `json:"adopt_avatar,omitempty"`
|
|
}
|
|
|
|
func (h *AuthHandler) pendingIdentityService() (*service.AuthPendingIdentityService, error) {
|
|
if h == nil || h.authService == nil || h.authService.EntClient() == nil {
|
|
return nil, infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready")
|
|
}
|
|
return service.NewAuthPendingIdentityService(h.authService.EntClient()), nil
|
|
}
|
|
|
|
func generateOAuthPendingBrowserSession() (string, error) {
|
|
return oauth.GenerateState()
|
|
}
|
|
|
|
func setOAuthPendingBrowserCookie(c *gin.Context, sessionKey string, secure bool) {
|
|
http.SetCookie(c.Writer, &http.Cookie{
|
|
Name: oauthPendingBrowserCookieName,
|
|
Value: encodeCookieValue(sessionKey),
|
|
Path: oauthPendingBrowserCookiePath,
|
|
MaxAge: oauthPendingCookieMaxAgeSec,
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
func clearOAuthPendingBrowserCookie(c *gin.Context, secure bool) {
|
|
http.SetCookie(c.Writer, &http.Cookie{
|
|
Name: oauthPendingBrowserCookieName,
|
|
Value: "",
|
|
Path: oauthPendingBrowserCookiePath,
|
|
MaxAge: -1,
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
func readOAuthPendingBrowserCookie(c *gin.Context) (string, error) {
|
|
return readCookieDecoded(c, oauthPendingBrowserCookieName)
|
|
}
|
|
|
|
func setOAuthPendingSessionCookie(c *gin.Context, sessionToken string, secure bool) {
|
|
http.SetCookie(c.Writer, &http.Cookie{
|
|
Name: oauthPendingSessionCookieName,
|
|
Value: encodeCookieValue(sessionToken),
|
|
Path: oauthPendingSessionCookiePath,
|
|
MaxAge: oauthPendingCookieMaxAgeSec,
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
func clearOAuthPendingSessionCookie(c *gin.Context, secure bool) {
|
|
http.SetCookie(c.Writer, &http.Cookie{
|
|
Name: oauthPendingSessionCookieName,
|
|
Value: "",
|
|
Path: oauthPendingSessionCookiePath,
|
|
MaxAge: -1,
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
func readOAuthPendingSessionCookie(c *gin.Context) (string, error) {
|
|
return readCookieDecoded(c, oauthPendingSessionCookieName)
|
|
}
|
|
|
|
func redirectToFrontendCallback(c *gin.Context, frontendCallback string) {
|
|
u, err := url.Parse(frontendCallback)
|
|
if err != nil {
|
|
c.Redirect(http.StatusFound, linuxDoOAuthDefaultRedirectTo)
|
|
return
|
|
}
|
|
if u.Scheme != "" && !strings.EqualFold(u.Scheme, "http") && !strings.EqualFold(u.Scheme, "https") {
|
|
c.Redirect(http.StatusFound, linuxDoOAuthDefaultRedirectTo)
|
|
return
|
|
}
|
|
u.Fragment = ""
|
|
c.Header("Cache-Control", "no-store")
|
|
c.Header("Pragma", "no-cache")
|
|
c.Redirect(http.StatusFound, u.String())
|
|
}
|
|
|
|
func (h *AuthHandler) createOAuthPendingSession(c *gin.Context, payload oauthPendingSessionPayload) error {
|
|
svc, err := h.pendingIdentityService()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
session, err := svc.CreatePendingSession(c.Request.Context(), service.CreatePendingAuthSessionInput{
|
|
Intent: strings.TrimSpace(payload.Intent),
|
|
Identity: payload.Identity,
|
|
TargetUserID: payload.TargetUserID,
|
|
ResolvedEmail: strings.TrimSpace(payload.ResolvedEmail),
|
|
RedirectTo: strings.TrimSpace(payload.RedirectTo),
|
|
BrowserSessionKey: strings.TrimSpace(payload.BrowserSessionKey),
|
|
UpstreamIdentityClaims: payload.UpstreamIdentityClaims,
|
|
LocalFlowState: map[string]any{
|
|
oauthCompletionResponseKey: payload.CompletionResponse,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return infraerrors.InternalServer("PENDING_AUTH_SESSION_CREATE_FAILED", "failed to create pending auth session").WithCause(err)
|
|
}
|
|
|
|
setOAuthPendingSessionCookie(c, session.SessionToken, isRequestHTTPS(c))
|
|
return nil
|
|
}
|
|
|
|
func readCompletionResponse(session map[string]any) (map[string]any, bool) {
|
|
if len(session) == 0 {
|
|
return nil, false
|
|
}
|
|
value, ok := session[oauthCompletionResponseKey]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
result, ok := value.(map[string]any)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
return result, true
|
|
}
|
|
|
|
func pendingSessionStringValue(values map[string]any, key string) string {
|
|
if len(values) == 0 {
|
|
return ""
|
|
}
|
|
raw, ok := values[key]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
value, ok := raw.(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(value)
|
|
}
|
|
|
|
func pendingSessionWantsInvitation(payload map[string]any) bool {
|
|
return strings.EqualFold(strings.TrimSpace(pendingSessionStringValue(payload, "error")), "invitation_required")
|
|
}
|
|
|
|
func (r oauthAdoptionDecisionRequest) hasDecision() bool {
|
|
return r.AdoptDisplayName != nil || r.AdoptAvatar != nil
|
|
}
|
|
|
|
func (r oauthAdoptionDecisionRequest) toServiceInput(sessionID int64) service.PendingIdentityAdoptionDecisionInput {
|
|
input := service.PendingIdentityAdoptionDecisionInput{
|
|
PendingAuthSessionID: sessionID,
|
|
}
|
|
if r.AdoptDisplayName != nil {
|
|
input.AdoptDisplayName = *r.AdoptDisplayName
|
|
}
|
|
if r.AdoptAvatar != nil {
|
|
input.AdoptAvatar = *r.AdoptAvatar
|
|
}
|
|
return input
|
|
}
|
|
|
|
func bindOptionalOAuthAdoptionDecision(c *gin.Context) (oauthAdoptionDecisionRequest, error) {
|
|
var req oauthAdoptionDecisionRequest
|
|
if c == nil || c.Request == nil || c.Request.Body == nil {
|
|
return req, nil
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
return req, nil
|
|
}
|
|
return req, err
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func persistPendingOAuthAdoptionDecision(
|
|
c *gin.Context,
|
|
svc *service.AuthPendingIdentityService,
|
|
sessionID int64,
|
|
req oauthAdoptionDecisionRequest,
|
|
) error {
|
|
if !req.hasDecision() {
|
|
return nil
|
|
}
|
|
if svc == nil {
|
|
return infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready")
|
|
}
|
|
if _, err := svc.UpsertAdoptionDecision(c.Request.Context(), req.toServiceInput(sessionID)); err != nil {
|
|
return infraerrors.InternalServer("PENDING_AUTH_ADOPTION_SAVE_FAILED", "failed to save oauth profile adoption decision").WithCause(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cloneOAuthMetadata(values map[string]any) map[string]any {
|
|
if len(values) == 0 {
|
|
return map[string]any{}
|
|
}
|
|
cloned := make(map[string]any, len(values))
|
|
for key, value := range values {
|
|
cloned[key] = value
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func normalizeAdoptedOAuthDisplayName(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if len([]rune(value)) > 100 {
|
|
value = string([]rune(value)[:100])
|
|
}
|
|
return value
|
|
}
|
|
|
|
func (h *AuthHandler) entClient() *dbent.Client {
|
|
if h == nil || h.authService == nil {
|
|
return nil
|
|
}
|
|
return h.authService.EntClient()
|
|
}
|
|
|
|
func (h *AuthHandler) upsertPendingOAuthAdoptionDecision(
|
|
c *gin.Context,
|
|
sessionID int64,
|
|
req oauthAdoptionDecisionRequest,
|
|
) (*dbent.IdentityAdoptionDecision, error) {
|
|
client := h.entClient()
|
|
if client == nil {
|
|
return nil, infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready")
|
|
}
|
|
|
|
existing, err := client.IdentityAdoptionDecision.Query().
|
|
Where(identityadoptiondecision.PendingAuthSessionIDEQ(sessionID)).
|
|
Only(c.Request.Context())
|
|
if err != nil && !dbent.IsNotFound(err) {
|
|
return nil, infraerrors.InternalServer("PENDING_AUTH_ADOPTION_LOAD_FAILED", "failed to load oauth profile adoption decision").WithCause(err)
|
|
}
|
|
if existing != nil && !req.hasDecision() {
|
|
return existing, nil
|
|
}
|
|
if existing == nil && !req.hasDecision() {
|
|
return nil, nil
|
|
}
|
|
|
|
input := service.PendingIdentityAdoptionDecisionInput{
|
|
PendingAuthSessionID: sessionID,
|
|
}
|
|
if existing != nil {
|
|
input.AdoptDisplayName = existing.AdoptDisplayName
|
|
input.AdoptAvatar = existing.AdoptAvatar
|
|
input.IdentityID = existing.IdentityID
|
|
}
|
|
if req.AdoptDisplayName != nil {
|
|
input.AdoptDisplayName = *req.AdoptDisplayName
|
|
}
|
|
if req.AdoptAvatar != nil {
|
|
input.AdoptAvatar = *req.AdoptAvatar
|
|
}
|
|
|
|
svc, err := h.pendingIdentityService()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
decision, err := svc.UpsertAdoptionDecision(c.Request.Context(), input)
|
|
if err != nil {
|
|
return nil, infraerrors.InternalServer("PENDING_AUTH_ADOPTION_SAVE_FAILED", "failed to save oauth profile adoption decision").WithCause(err)
|
|
}
|
|
return decision, nil
|
|
}
|
|
|
|
func resolvePendingOAuthTargetUserID(ctx context.Context, client *dbent.Client, session *dbent.PendingAuthSession) (int64, error) {
|
|
if session == nil {
|
|
return 0, infraerrors.BadRequest("PENDING_AUTH_SESSION_INVALID", "pending auth session is invalid")
|
|
}
|
|
if session.TargetUserID != nil && *session.TargetUserID > 0 {
|
|
return *session.TargetUserID, nil
|
|
}
|
|
email := strings.TrimSpace(session.ResolvedEmail)
|
|
if email == "" {
|
|
return 0, infraerrors.BadRequest("PENDING_AUTH_TARGET_USER_MISSING", "pending auth target user is missing")
|
|
}
|
|
|
|
userEntity, err := client.User.Query().
|
|
Where(dbuser.EmailEQ(email)).
|
|
Only(ctx)
|
|
if err != nil {
|
|
if dbent.IsNotFound(err) {
|
|
return 0, infraerrors.InternalServer("PENDING_AUTH_TARGET_USER_NOT_FOUND", "pending auth target user was not found")
|
|
}
|
|
return 0, err
|
|
}
|
|
return userEntity.ID, nil
|
|
}
|
|
|
|
func oauthIdentityIssuer(session *dbent.PendingAuthSession) *string {
|
|
if session == nil {
|
|
return nil
|
|
}
|
|
switch strings.TrimSpace(session.ProviderType) {
|
|
case "oidc":
|
|
issuer := strings.TrimSpace(session.ProviderKey)
|
|
if issuer == "" {
|
|
issuer = pendingSessionStringValue(session.UpstreamIdentityClaims, "issuer")
|
|
}
|
|
if issuer == "" {
|
|
return nil
|
|
}
|
|
return &issuer
|
|
default:
|
|
issuer := pendingSessionStringValue(session.UpstreamIdentityClaims, "issuer")
|
|
if issuer == "" {
|
|
return nil
|
|
}
|
|
return &issuer
|
|
}
|
|
}
|
|
|
|
func ensurePendingOAuthIdentityForUser(ctx context.Context, tx *dbent.Tx, session *dbent.PendingAuthSession, userID int64) (*dbent.AuthIdentity, error) {
|
|
client := tx.Client()
|
|
identity, err := client.AuthIdentity.Query().
|
|
Where(
|
|
authidentity.ProviderTypeEQ(strings.TrimSpace(session.ProviderType)),
|
|
authidentity.ProviderKeyEQ(strings.TrimSpace(session.ProviderKey)),
|
|
authidentity.ProviderSubjectEQ(strings.TrimSpace(session.ProviderSubject)),
|
|
).
|
|
Only(ctx)
|
|
if err != nil && !dbent.IsNotFound(err) {
|
|
return nil, err
|
|
}
|
|
if identity != nil {
|
|
if identity.UserID != userID {
|
|
return nil, infraerrors.Conflict("AUTH_IDENTITY_OWNERSHIP_CONFLICT", "auth identity already belongs to another user")
|
|
}
|
|
return identity, nil
|
|
}
|
|
|
|
create := client.AuthIdentity.Create().
|
|
SetUserID(userID).
|
|
SetProviderType(strings.TrimSpace(session.ProviderType)).
|
|
SetProviderKey(strings.TrimSpace(session.ProviderKey)).
|
|
SetProviderSubject(strings.TrimSpace(session.ProviderSubject)).
|
|
SetMetadata(cloneOAuthMetadata(session.UpstreamIdentityClaims))
|
|
if issuer := oauthIdentityIssuer(session); issuer != nil {
|
|
create = create.SetIssuer(strings.TrimSpace(*issuer))
|
|
}
|
|
return create.Save(ctx)
|
|
}
|
|
|
|
func shouldBindPendingOAuthIdentity(session *dbent.PendingAuthSession, decision *dbent.IdentityAdoptionDecision) bool {
|
|
if session == nil || decision == nil {
|
|
return false
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(session.Intent), "bind_current_user") {
|
|
return true
|
|
}
|
|
return decision.AdoptDisplayName || decision.AdoptAvatar
|
|
}
|
|
|
|
func applyPendingOAuthAdoption(
|
|
ctx context.Context,
|
|
client *dbent.Client,
|
|
session *dbent.PendingAuthSession,
|
|
decision *dbent.IdentityAdoptionDecision,
|
|
overrideUserID *int64,
|
|
) error {
|
|
if client == nil || session == nil || decision == nil {
|
|
return nil
|
|
}
|
|
if !shouldBindPendingOAuthIdentity(session, decision) {
|
|
return nil
|
|
}
|
|
|
|
targetUserID := int64(0)
|
|
if overrideUserID != nil && *overrideUserID > 0 {
|
|
targetUserID = *overrideUserID
|
|
} else {
|
|
resolvedUserID, err := resolvePendingOAuthTargetUserID(ctx, client, session)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
targetUserID = resolvedUserID
|
|
}
|
|
|
|
adoptedDisplayName := ""
|
|
if decision.AdoptDisplayName {
|
|
adoptedDisplayName = normalizeAdoptedOAuthDisplayName(pendingSessionStringValue(session.UpstreamIdentityClaims, "suggested_display_name"))
|
|
}
|
|
adoptedAvatarURL := ""
|
|
if decision.AdoptAvatar {
|
|
adoptedAvatarURL = pendingSessionStringValue(session.UpstreamIdentityClaims, "suggested_avatar_url")
|
|
}
|
|
|
|
tx, err := client.Tx(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
if decision.AdoptDisplayName && adoptedDisplayName != "" {
|
|
if err := tx.Client().User.UpdateOneID(targetUserID).
|
|
SetUsername(adoptedDisplayName).
|
|
Exec(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
identity, err := ensurePendingOAuthIdentityForUser(ctx, tx, session, targetUserID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
metadata := cloneOAuthMetadata(identity.Metadata)
|
|
for key, value := range session.UpstreamIdentityClaims {
|
|
metadata[key] = value
|
|
}
|
|
if decision.AdoptDisplayName && adoptedDisplayName != "" {
|
|
metadata["display_name"] = adoptedDisplayName
|
|
}
|
|
if decision.AdoptAvatar && adoptedAvatarURL != "" {
|
|
metadata["avatar_url"] = adoptedAvatarURL
|
|
}
|
|
|
|
updateIdentity := tx.Client().AuthIdentity.UpdateOneID(identity.ID).SetMetadata(metadata)
|
|
if issuer := oauthIdentityIssuer(session); issuer != nil {
|
|
updateIdentity = updateIdentity.SetIssuer(strings.TrimSpace(*issuer))
|
|
}
|
|
if _, err := updateIdentity.Save(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if decision.IdentityID == nil || *decision.IdentityID != identity.ID {
|
|
if _, err := tx.Client().IdentityAdoptionDecision.UpdateOneID(decision.ID).
|
|
SetIdentityID(identity.ID).
|
|
Save(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func applySuggestedProfileToCompletionResponse(payload map[string]any, upstream map[string]any) {
|
|
if len(payload) == 0 || len(upstream) == 0 {
|
|
return
|
|
}
|
|
|
|
displayName := pendingSessionStringValue(upstream, "suggested_display_name")
|
|
avatarURL := pendingSessionStringValue(upstream, "suggested_avatar_url")
|
|
|
|
if displayName != "" {
|
|
if _, exists := payload["suggested_display_name"]; !exists {
|
|
payload["suggested_display_name"] = displayName
|
|
}
|
|
}
|
|
if avatarURL != "" {
|
|
if _, exists := payload["suggested_avatar_url"]; !exists {
|
|
payload["suggested_avatar_url"] = avatarURL
|
|
}
|
|
}
|
|
if displayName != "" || avatarURL != "" {
|
|
payload["adoption_required"] = true
|
|
}
|
|
}
|
|
|
|
// ExchangePendingOAuthCompletion redeems a pending OAuth browser session into a frontend-safe payload.
|
|
// POST /api/v1/auth/oauth/pending/exchange
|
|
func (h *AuthHandler) ExchangePendingOAuthCompletion(c *gin.Context) {
|
|
secureCookie := isRequestHTTPS(c)
|
|
clearCookies := func() {
|
|
clearOAuthPendingSessionCookie(c, secureCookie)
|
|
clearOAuthPendingBrowserCookie(c, secureCookie)
|
|
}
|
|
adoptionDecision, err := bindOptionalOAuthAdoptionDecision(c)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
sessionToken, err := readOAuthPendingSessionCookie(c)
|
|
if err != nil || strings.TrimSpace(sessionToken) == "" {
|
|
clearCookies()
|
|
response.ErrorFrom(c, service.ErrPendingAuthSessionNotFound)
|
|
return
|
|
}
|
|
browserSessionKey, err := readOAuthPendingBrowserCookie(c)
|
|
if err != nil || strings.TrimSpace(browserSessionKey) == "" {
|
|
clearCookies()
|
|
response.ErrorFrom(c, service.ErrPendingAuthBrowserMismatch)
|
|
return
|
|
}
|
|
|
|
svc, err := h.pendingIdentityService()
|
|
if err != nil {
|
|
clearCookies()
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
session, err := svc.GetBrowserSession(c.Request.Context(), sessionToken, browserSessionKey)
|
|
if err != nil {
|
|
clearCookies()
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
payload, ok := readCompletionResponse(session.LocalFlowState)
|
|
if !ok {
|
|
clearCookies()
|
|
response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_COMPLETION_INVALID", "pending auth completion payload is invalid"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(session.RedirectTo) != "" {
|
|
if _, exists := payload["redirect"]; !exists {
|
|
payload["redirect"] = session.RedirectTo
|
|
}
|
|
}
|
|
applySuggestedProfileToCompletionResponse(payload, session.UpstreamIdentityClaims)
|
|
|
|
if pendingSessionWantsInvitation(payload) {
|
|
if adoptionDecision.hasDecision() {
|
|
decision, err := h.upsertPendingOAuthAdoptionDecision(c, session.ID, adoptionDecision)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
_ = decision
|
|
}
|
|
response.Success(c, payload)
|
|
return
|
|
}
|
|
if !adoptionDecision.hasDecision() {
|
|
response.Success(c, payload)
|
|
return
|
|
}
|
|
decision, err := h.upsertPendingOAuthAdoptionDecision(c, session.ID, adoptionDecision)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
if err := applyPendingOAuthAdoption(c.Request.Context(), h.entClient(), session, decision, session.TargetUserID); err != nil {
|
|
response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_ADOPTION_APPLY_FAILED", "failed to apply oauth profile adoption").WithCause(err))
|
|
return
|
|
}
|
|
|
|
if _, err := svc.ConsumeBrowserSession(c.Request.Context(), sessionToken, browserSessionKey); err != nil {
|
|
clearCookies()
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
clearCookies()
|
|
response.Success(c, payload)
|
|
}
|