feat: publish v1.0.0-rc.36-custom.1
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
This commit is contained in:
commit
bb7968b612
2381 files changed
+551494
No files matched your search
@@ -0,0 +1,499 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/oauth"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
VerificationMethodTwoFA = "2fa"
|
||||
VerificationMethodPasskey = "passkey"
|
||||
VerificationMethodPassword = "password"
|
||||
VerificationMethodOAuth = "oauth"
|
||||
VerificationMethodSession = "session"
|
||||
VerificationScopeChannelKeyRead = "channel.key.read"
|
||||
VerificationScopePasskeyRegister = "passkey.register"
|
||||
VerificationScopePasskeyDelete = "passkey.delete"
|
||||
VerificationScopeTwoFASetup = "2fa.setup"
|
||||
VerificationScopeTwoFADisable = "2fa.disable"
|
||||
VerificationScopeTwoFABackupCodes = "2fa.backup_codes.regenerate"
|
||||
VerificationScopeLogin = "auth.login"
|
||||
VerificationScopeAccessTokenGenerate = "access_token.generate"
|
||||
VerificationScopeAccessTokenRevoke = "access_token.revoke"
|
||||
VerificationScopeAccountBind = "account.binding.bind"
|
||||
VerificationScopeAccountUnbind = "account.binding.unbind"
|
||||
VerificationScopePasswordSet = "account.password.set"
|
||||
VerificationScopePasswordChange = "account.password.change"
|
||||
VerificationScopeAccountDelete = "account.delete"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrVerificationFailed = errors.New("Verification failed. Please try again.")
|
||||
ErrVerificationUnavailable = errors.New("This verification method is currently unavailable.")
|
||||
ErrVerificationLocked = errors.New("Two-factor authentication is temporarily locked.")
|
||||
ErrVerificationFlowRequired = errors.New("This verification method requires its dedicated verification flow.")
|
||||
ErrVerificationContextInvalid = errors.New("The action details are invalid.")
|
||||
ErrVerificationForbidden = errors.New("You do not have permission to perform this action.")
|
||||
)
|
||||
|
||||
// VerificationOperation describes the action being authorized, independently of
|
||||
// its authentication method. Each scope owns a strict, normalized context schema.
|
||||
type VerificationOperation struct {
|
||||
Scope string `json:"scope"`
|
||||
Context json.RawMessage `json:"context,omitempty"`
|
||||
}
|
||||
|
||||
type ChannelKeyReadContext struct {
|
||||
ChannelID int `json:"channel_id"`
|
||||
}
|
||||
|
||||
type AccountBindingContext struct {
|
||||
Provider string `json:"provider"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
type AccountUnbindingContext struct {
|
||||
ProviderID int `json:"provider_id"`
|
||||
}
|
||||
|
||||
// VerificationBinding contains no original operation parameters. It can safely
|
||||
// travel through a signed proof or a server-owned interactive verification flow.
|
||||
type VerificationBinding struct {
|
||||
Scope string `json:"scope"`
|
||||
ContextHash string `json:"context_hash"`
|
||||
}
|
||||
|
||||
func BindVerificationOperation(operation VerificationOperation) (VerificationBinding, error) {
|
||||
var fields map[string]json.RawMessage
|
||||
if len(operation.Context) > 0 {
|
||||
if common.GetJsonType(operation.Context) != "object" || common.Unmarshal(operation.Context, &fields) != nil {
|
||||
return VerificationBinding{}, ErrVerificationContextInvalid
|
||||
}
|
||||
}
|
||||
var normalized any
|
||||
switch operation.Scope {
|
||||
case VerificationScopeChannelKeyRead:
|
||||
var context ChannelKeyReadContext
|
||||
if len(fields) != 1 || common.Unmarshal(fields["channel_id"], &context.ChannelID) != nil || context.ChannelID <= 0 {
|
||||
return VerificationBinding{}, ErrVerificationContextInvalid
|
||||
}
|
||||
normalized = context
|
||||
case VerificationScopeAccountBind:
|
||||
var context AccountBindingContext
|
||||
if common.Unmarshal(operation.Context, &context) != nil {
|
||||
return VerificationBinding{}, ErrVerificationContextInvalid
|
||||
}
|
||||
context.Provider = strings.TrimSpace(context.Provider)
|
||||
switch context.Provider {
|
||||
case "email":
|
||||
context.Email = model.NormalizeEmail(context.Email)
|
||||
if len(fields) != 2 || common.Validate.Var(context.Email, "required,email") != nil || context.Code != "" {
|
||||
return VerificationBinding{}, ErrVerificationContextInvalid
|
||||
}
|
||||
case "wechat":
|
||||
context.Code = strings.TrimSpace(context.Code)
|
||||
if len(fields) != 2 || context.Code == "" || len(context.Code) > 128 || context.Email != "" {
|
||||
return VerificationBinding{}, ErrVerificationContextInvalid
|
||||
}
|
||||
default:
|
||||
if len(fields) != 1 || context.Provider == "" || len(context.Provider) > 64 || oauth.GetProvider(context.Provider) == nil {
|
||||
return VerificationBinding{}, ErrVerificationContextInvalid
|
||||
}
|
||||
}
|
||||
normalized = context
|
||||
case VerificationScopeAccountUnbind:
|
||||
var context AccountUnbindingContext
|
||||
if len(fields) != 1 || common.Unmarshal(fields["provider_id"], &context.ProviderID) != nil || context.ProviderID <= 0 {
|
||||
return VerificationBinding{}, ErrVerificationContextInvalid
|
||||
}
|
||||
normalized = context
|
||||
case VerificationScopePasskeyRegister, VerificationScopePasskeyDelete, VerificationScopeTwoFASetup,
|
||||
VerificationScopeTwoFADisable, VerificationScopeTwoFABackupCodes,
|
||||
VerificationScopeAccessTokenGenerate, VerificationScopeAccessTokenRevoke,
|
||||
VerificationScopePasswordSet, VerificationScopePasswordChange, VerificationScopeAccountDelete:
|
||||
if len(fields) != 0 {
|
||||
return VerificationBinding{}, ErrVerificationContextInvalid
|
||||
}
|
||||
normalized = struct{}{}
|
||||
default:
|
||||
return VerificationBinding{}, ErrProofScope
|
||||
}
|
||||
payload, err := common.Marshal(struct {
|
||||
Scope string `json:"scope"`
|
||||
Context any `json:"context"`
|
||||
}{Scope: operation.Scope, Context: normalized})
|
||||
if err != nil {
|
||||
return VerificationBinding{}, err
|
||||
}
|
||||
return VerificationBinding{
|
||||
Scope: operation.Scope,
|
||||
ContextHash: common.GenerateHMACWithKey(authSigningKey("verification-context"), string(payload)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type SecurityProof struct {
|
||||
ProofToken string `json:"proof_token"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Method string `json:"method"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
type VerificationMethodOption struct {
|
||||
Method string `json:"method"`
|
||||
Available bool `json:"available"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type VerificationOAuthProvider struct {
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
type VerificationRequirements struct {
|
||||
Scope string `json:"scope"`
|
||||
Methods []VerificationMethodOption `json:"methods"`
|
||||
OAuthProviders []VerificationOAuthProvider `json:"oauth_providers"`
|
||||
PasswordEncryptionEnabled bool `json:"password_encryption_enabled"`
|
||||
}
|
||||
|
||||
// securityVerificationPolicy is the only operation-to-method policy. Device
|
||||
// support and disabled providers never turn an enrolled factor into an absent one.
|
||||
func securityVerificationPolicy(scope string, state model.UserVerificationState) ([]VerificationMethodOption, error) {
|
||||
var methods []string
|
||||
if state.HasTwoFA {
|
||||
methods = append(methods, VerificationMethodTwoFA)
|
||||
}
|
||||
if state.HasPasskey {
|
||||
methods = append(methods, VerificationMethodPasskey)
|
||||
}
|
||||
switch scope {
|
||||
case VerificationScopeChannelKeyRead, VerificationScopePasskeyDelete, VerificationScopeLogin:
|
||||
case VerificationScopeTwoFADisable, VerificationScopeTwoFABackupCodes:
|
||||
if !state.HasTwoFA {
|
||||
return nil, model.ErrTwoFANotEnabled
|
||||
}
|
||||
case VerificationScopePasskeyRegister, VerificationScopeTwoFASetup,
|
||||
VerificationScopeAccessTokenGenerate, VerificationScopeAccessTokenRevoke,
|
||||
VerificationScopeAccountBind, VerificationScopeAccountUnbind,
|
||||
VerificationScopePasswordSet, VerificationScopePasswordChange, VerificationScopeAccountDelete:
|
||||
if scope == VerificationScopeAccountDelete && state.Role == common.RoleRootUser {
|
||||
return nil, ErrVerificationForbidden
|
||||
}
|
||||
if scope == VerificationScopeTwoFASetup && state.HasTwoFA {
|
||||
return nil, model.ErrTwoFAAlreadyEnabled
|
||||
}
|
||||
if (scope == VerificationScopePasswordSet && state.HasPassword) || (scope == VerificationScopePasswordChange && !state.HasPassword) {
|
||||
return nil, ErrVerificationForbidden
|
||||
}
|
||||
if len(methods) == 0 {
|
||||
if state.HasPassword {
|
||||
methods = []string{VerificationMethodPassword}
|
||||
} else {
|
||||
methods = []string{VerificationMethodOAuth}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, ErrProofScope
|
||||
}
|
||||
options := make([]VerificationMethodOption, 0, len(methods))
|
||||
for _, method := range methods {
|
||||
option := VerificationMethodOption{Method: method, Available: true}
|
||||
if method == VerificationMethodTwoFA && state.TwoFALocked {
|
||||
option.Available, option.Reason = false, ErrVerificationLocked.Error()
|
||||
}
|
||||
if !system_setting.GetPasskeySettings().Enabled && (method == VerificationMethodPasskey || scope == VerificationScopePasskeyRegister) {
|
||||
option.Available, option.Reason = false, "Passkey authentication is disabled."
|
||||
}
|
||||
options = append(options, option)
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func GetVerificationRequirements(identity AuthIdentity, scope string) (*VerificationRequirements, error) {
|
||||
if scope == VerificationScopeLogin {
|
||||
return nil, ErrProofScope
|
||||
}
|
||||
state, err := model.GetUserVerificationState(identity.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if state.Status != common.UserStatusEnabled || state.AuthVersion != identity.UserAuthVersion {
|
||||
return nil, ErrAuthTokenInvalid
|
||||
}
|
||||
if scope == VerificationScopeChannelKeyRead && state.Role != common.RoleRootUser {
|
||||
return nil, ErrVerificationForbidden
|
||||
}
|
||||
methods, err := securityVerificationPolicy(scope, *state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requirements := &VerificationRequirements{Scope: scope, Methods: methods, OAuthProviders: []VerificationOAuthProvider{}, PasswordEncryptionEnabled: common.PasswordLoginEncryptionEnabled}
|
||||
for i := range methods {
|
||||
if methods[i].Method == VerificationMethodPassword && !common.PasswordLoginEnabled {
|
||||
switch scope {
|
||||
case VerificationScopeAccountBind, VerificationScopeAccountUnbind, VerificationScopePasswordSet, VerificationScopePasswordChange, VerificationScopeAccountDelete:
|
||||
methods[i].Available, methods[i].Reason = false, "Password authentication is disabled."
|
||||
}
|
||||
}
|
||||
if methods[i].Method != VerificationMethodOAuth {
|
||||
continue
|
||||
}
|
||||
user, err := model.GetUserById(identity.UserID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requirements.OAuthProviders, err = verificationOAuthProviders(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(requirements.OAuthProviders) == 0 {
|
||||
methods[i].Available, methods[i].Reason = false, "No linked OAuth provider is available."
|
||||
if user.TelegramId != "" {
|
||||
if err := oauth.TelegramConfigurationError(); err != nil {
|
||||
methods[i].Reason = err.Error()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return requirements, nil
|
||||
}
|
||||
|
||||
func verificationOAuthProviders(user *model.User) ([]VerificationOAuthProvider, error) {
|
||||
bindings := map[int]string{}
|
||||
if len(oauth.GetEnabledCustomProviders()) > 0 {
|
||||
stored, err := model.GetUserOAuthBindingsByUserId(user.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, binding := range stored {
|
||||
bindings[binding.ProviderId] = binding.ProviderUserId
|
||||
}
|
||||
}
|
||||
providers := []VerificationOAuthProvider{}
|
||||
for slug, provider := range oauth.GetAllProviders() {
|
||||
if !provider.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
var userID string
|
||||
if custom, ok := provider.(*oauth.GenericOAuthProvider); ok {
|
||||
userID = bindings[custom.GetProviderId()]
|
||||
} else {
|
||||
switch provider.ProviderUserIDColumn() {
|
||||
case "github_id":
|
||||
userID = user.GitHubId
|
||||
case "discord_id":
|
||||
userID = user.DiscordId
|
||||
case "oidc_id":
|
||||
userID = user.OidcId
|
||||
case "linux_do_id":
|
||||
userID = user.LinuxDOId
|
||||
case "telegram_id":
|
||||
userID = user.TelegramId
|
||||
}
|
||||
}
|
||||
if userID != "" {
|
||||
providers = append(providers, VerificationOAuthProvider{Slug: slug, Name: provider.GetName(), UserID: userID})
|
||||
}
|
||||
}
|
||||
sort.Slice(providers, func(i, j int) bool { return providers[i].Slug < providers[j].Slug })
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func RequireVerificationMethod(identity AuthIdentity, scope, method string) (*VerificationRequirements, error) {
|
||||
requirements, err := GetVerificationRequirements(identity, scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, option := range requirements.Methods {
|
||||
if option.Method != method {
|
||||
continue
|
||||
}
|
||||
if !option.Available {
|
||||
return nil, ErrVerificationUnavailable
|
||||
}
|
||||
return requirements, nil
|
||||
}
|
||||
return nil, ErrProofMethod
|
||||
}
|
||||
|
||||
// CompleteSecurityVerification runs after the concrete authentication ceremony.
|
||||
// Recheck the session and policy after potentially slow external authentication.
|
||||
func CompleteSecurityVerification(identity AuthIdentity, binding VerificationBinding, method string) (*SecurityProof, error) {
|
||||
if _, _, err := ValidateLoginSession(identity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := RequireVerificationMethod(identity, binding.Scope, method); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, expiresAt, err := IssueSecurityProof(identity, method, binding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SecurityProof{ProofToken: token, ExpiresAt: expiresAt, Method: method, Scope: binding.Scope}, nil
|
||||
}
|
||||
|
||||
// ConsumeOperationProof accepts a proof at most once. The consumption commits
|
||||
// before the caller performs its action; an action failure must not restore it.
|
||||
func ConsumeOperationProof(raw string, identity AuthIdentity, operation VerificationOperation) (*model.AuthFlowAuthorization, error) {
|
||||
binding, err := BindVerificationOperation(operation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, err := verifySecurityProof(raw, identity, binding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, _, err := ValidateLoginSession(identity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := RequireVerificationMethod(identity, binding.Scope, claims.Method); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flow, err := model.ConsumeAuthFlowWithAction(claims.ID, model.AuthFlowMatch{
|
||||
Purpose: model.AuthFlowPurposeSecurityProof, UserId: identity.UserID, SessionId: identity.SessionID,
|
||||
}, func(tx *gorm.DB, _ *model.AuthFlow) error {
|
||||
return model.ValidateAuthSessionWithTx(tx, identity)
|
||||
})
|
||||
switch {
|
||||
case errors.Is(err, model.ErrAuthFlowConsumed):
|
||||
return nil, ErrProofConsumed
|
||||
case errors.Is(err, model.ErrAuthFlowExpired):
|
||||
return nil, ErrAuthTokenExpired
|
||||
case errors.Is(err, model.ErrAuthFlowInvalid):
|
||||
return nil, ErrAuthTokenInvalid
|
||||
case err != nil:
|
||||
return nil, err
|
||||
}
|
||||
return &model.AuthFlowAuthorization{
|
||||
AuthSessionIdentity: identity, ProofID: flow.Id, Scope: binding.Scope,
|
||||
ContextHash: binding.ContextHash, Method: claims.Method,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateFlowAuthorization permits a dedicated configuration flow to outlive
|
||||
// its consumed proof, while retaining its operation, session and method policy.
|
||||
func ValidateFlowAuthorization(identity AuthIdentity, operation VerificationOperation, authorization *model.AuthFlowAuthorization) error {
|
||||
if authorization == nil || authorization.ProofID <= 0 || authorization.AuthSessionIdentity != identity {
|
||||
return model.ErrAuthFlowInvalid
|
||||
}
|
||||
binding, err := BindVerificationOperation(operation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if authorization.Scope != binding.Scope || !hmac.Equal([]byte(authorization.ContextHash), []byte(binding.ContextHash)) {
|
||||
return model.ErrAuthFlowInvalid
|
||||
}
|
||||
if _, _, err := ValidateLoginSession(identity); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = RequireVerificationMethod(identity, binding.Scope, authorization.Method)
|
||||
return err
|
||||
}
|
||||
|
||||
type VerificationInput struct {
|
||||
Method string `json:"method"`
|
||||
Scope string `json:"scope"`
|
||||
Context json.RawMessage `json:"context,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
PasswordEncrypted string `json:"password_encrypted,omitempty"`
|
||||
EncryptionKeyID string `json:"encryption_key_id,omitempty"`
|
||||
}
|
||||
|
||||
func VerifySecurityInput(identity AuthIdentity, input VerificationInput) (*SecurityProof, error) {
|
||||
binding, err := BindVerificationOperation(VerificationOperation{Scope: input.Scope, Context: input.Context})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := RequireVerificationMethod(identity, input.Scope, input.Method); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch input.Method {
|
||||
case VerificationMethodPassword:
|
||||
password := input.Password
|
||||
if common.PasswordLoginEncryptionEnabled {
|
||||
var err error
|
||||
password, err = common.DecryptPassword(input.PasswordEncrypted, input.EncryptionKeyID)
|
||||
if err != nil {
|
||||
return nil, ErrVerificationFailed
|
||||
}
|
||||
}
|
||||
user, err := model.GetUserById(identity.UserID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if password == "" || user.Password == "" || !common.ValidatePasswordAndHash(password, user.Password) {
|
||||
return nil, ErrVerificationFailed
|
||||
}
|
||||
case VerificationMethodTwoFA:
|
||||
if input.Scope == VerificationScopeTwoFABackupCodes {
|
||||
if _, err := common.ValidateNumericCode(input.Code); err != nil {
|
||||
return nil, ErrVerificationFailed
|
||||
}
|
||||
}
|
||||
twoFA, err := model.GetTwoFAByUserId(identity.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := VerifyTwoFactorCode(twoFA, input.Code); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case VerificationMethodPasskey, VerificationMethodOAuth:
|
||||
return nil, ErrVerificationFlowRequired
|
||||
default:
|
||||
return nil, ErrProofMethod
|
||||
}
|
||||
return CompleteSecurityVerification(identity, binding, input.Method)
|
||||
}
|
||||
|
||||
// VerifyTwoFactorCode classifies the input before verification so one failed
|
||||
// submission cannot increment the failure counter for both TOTP and backup codes.
|
||||
func VerifyTwoFactorCode(twoFA *model.TwoFA, code string) error {
|
||||
if twoFA == nil || !twoFA.IsEnabled {
|
||||
return model.ErrTwoFANotEnabled
|
||||
}
|
||||
if twoFA.IsLocked() {
|
||||
return ErrVerificationLocked
|
||||
}
|
||||
code = strings.TrimSpace(code)
|
||||
var valid bool
|
||||
var err error
|
||||
if numeric, numericErr := common.ValidateNumericCode(code); numericErr == nil {
|
||||
valid, err = twoFA.ValidateTOTPAndUpdateUsage(numeric)
|
||||
} else if common.ValidateBackupCode(code) {
|
||||
valid, err = twoFA.ValidateBackupCodeAndUpdateUsage(code)
|
||||
} else {
|
||||
err = twoFA.IncrementFailedAttempts()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return ErrVerificationFailed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetOAuthVerificationBinding(identity AuthIdentity, scope, provider string) (string, error) {
|
||||
requirements, err := RequireVerificationMethod(identity, scope, VerificationMethodOAuth)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, linked := range requirements.OAuthProviders {
|
||||
if linked.Slug == provider {
|
||||
return linked.UserID, nil
|
||||
}
|
||||
}
|
||||
return "", ErrVerificationUnavailable
|
||||
}
|
||||
Reference in new issue
Block a user