Skip to content

Commit

Permalink
refactor(core): kratos error handling (#4605)
Browse files Browse the repository at this point in the history
  • Loading branch information
dolcalmi authored Oct 4, 2024
1 parent 35fc43b commit 5f25b73
Show file tree
Hide file tree
Showing 19 changed files with 187 additions and 176 deletions.
10 changes: 5 additions & 5 deletions core/api/src/app/authentication/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
AuthWithPhonePasswordlessService,
AuthWithUsernamePasswordDeviceIdService,
IdentityRepository,
PhoneAccountAlreadyExistsNeedToSweepFundsError,
} from "@/services/kratos"

import { LedgerService } from "@/services/ledger"
Expand All @@ -44,9 +43,9 @@ import { IPMetadataAuthorizer } from "@/domain/accounts-ips/ip-metadata-authoriz
import { getAccountsOnboardConfig, getDefaultAccountsConfig } from "@/config"

import {
UnauthorizedIPForOnboardingError,
MissingIPMetadataError,
InvalidIpMetadataError,
MissingIPMetadataError,
UnauthorizedIPForOnboardingError,
} from "@/domain/errors"
import {
InvalidPhoneForOnboardingError,
Expand All @@ -55,10 +54,11 @@ import {
import { IpFetcher } from "@/services/ipfetcher"

import { IpFetcherServiceError } from "@/domain/ipfetcher"
import { ErrorLevel } from "@/domain/shared"
import { consumeLimiter } from "@/services/rate-limit"
import { RateLimitConfig } from "@/domain/rate-limit"
import { RateLimiterExceededError } from "@/domain/rate-limit/errors"
import { ErrorLevel } from "@/domain/shared"
import { consumeLimiter } from "@/services/rate-limit"
import { PhoneAccountAlreadyExistsNeedToSweepFundsError } from "@/domain/kratos"

export const loginWithPhoneToken = async ({
phone,
Expand Down
6 changes: 2 additions & 4 deletions core/api/src/app/authentication/logout.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { removeDeviceTokens } from "@/app/users/remove-device-tokens"
import {
AuthWithPhonePasswordlessService,
MissingSessionIdError,
} from "@/services/kratos"
import { MissingSessionIdError } from "@/domain/kratos"
import { AuthWithPhonePasswordlessService } from "@/services/kratos"

export const logoutToken = async ({
userId,
Expand Down
4 changes: 2 additions & 2 deletions core/api/src/app/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import * as UserErrors from "@/domain/users/errors"
import * as WalletInvoiceErrors from "@/domain/wallet-invoices/errors"
import * as SupportError from "@/domain/support/errors"
import * as OathkeeperError from "@/domain/oathkeeper/errors"
import * as KratosErrors from "@/domain/kratos/errors"

import * as LedgerFacadeErrors from "@/services/ledger/domain/errors"
import * as KratosErrors from "@/services/kratos/errors"
import * as BriaEventErrors from "@/services/bria/errors"
import * as SvixErrors from "@/services/svix/errors"

Expand Down Expand Up @@ -55,8 +55,8 @@ export const ApplicationErrors = {
...WalletInvoiceErrors,
...SupportError,
...OathkeeperError,

...KratosErrors,

...LedgerFacadeErrors,
...BriaEventErrors,
...SvixErrors,
Expand Down
45 changes: 45 additions & 0 deletions core/api/src/domain/kratos/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AuthenticationError } from "@/domain/authentication/errors"
import { ErrorLevel } from "@/domain/shared"

export class KratosError extends AuthenticationError {}

export class MissingSessionIdError extends KratosError {
level = ErrorLevel.Critical
}

export class AuthenticationKratosError extends KratosError {}
export class AuthorizationKratosError extends KratosError {}
export class ExtendSessionKratosError extends KratosError {}
export class InvalidIdentitySessionKratosError extends KratosError {}

export class SessionRefreshRequiredError extends KratosError {}

export class EmailAlreadyExistsError extends KratosError {}

export class PhoneAccountAlreadyExistsError extends KratosError {
level = ErrorLevel.Info
}

export class PhoneAccountAlreadyExistsNeedToSweepFundsError extends KratosError {
level = ErrorLevel.Info
}

export class MissingCreatedAtKratosError extends KratosError {
level = ErrorLevel.Critical
}

export class MissingExpiredAtKratosError extends KratosError {
level = ErrorLevel.Critical
}

export class MissingTotpKratosError extends KratosError {
level = ErrorLevel.Critical
}

export class IncompatibleSchemaUpgradeError extends KratosError {}

export class CodeExpiredKratosError extends KratosError {}

export class UnknownKratosError extends KratosError {
level = ErrorLevel.Critical
}
1 change: 1 addition & 0 deletions core/api/src/domain/kratos/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./errors"
1 change: 1 addition & 0 deletions core/api/src/domain/kratos/index.types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
type KratosError = import("./errors").KratosError
1 change: 1 addition & 0 deletions core/api/src/graphql/error-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,7 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => {
case "BriaPayloadError":
case "KratosError":
case "AuthenticationKratosError":
case "AuthorizationKratosError":
case "ExtendSessionKratosError":
case "MultipleCurrenciesForSingleCurrencyOperationError":
case "MattermostError":
Expand Down
51 changes: 26 additions & 25 deletions core/api/src/services/kratos/auth-email-no-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,30 @@ import { isAxiosError } from "axios"

import { UpdateIdentityBody } from "@ory/client"

import {
CodeExpiredKratosError,
EmailAlreadyExistsError,
IncompatibleSchemaUpgradeError,
InvalidIdentitySessionKratosError,
KratosError,
UnknownKratosError,
} from "./errors"
import { kratosAdmin, kratosPublic, toDomainIdentityEmailPhone } from "./private"
import { SchemaIdType } from "./schema"

import { IdentityRepository } from "./identity"

import { checkedToEmailAddress } from "@/domain/users"
import { wrapAsyncFunctionsToRunInSpan } from "@/services/tracing"
import { handleKratosErrors } from "./errors"

import { KRATOS_MASTER_USER_PASSWORD } from "@/config"
import {
EmailCodeExpiredError,
EmailCodeInvalidError,
EmailUnverifiedError,
EmailValidationSubmittedTooOftenError,
LikelyUserAlreadyExistError,
} from "@/domain/authentication/errors"
import { KRATOS_MASTER_USER_PASSWORD } from "@/config"
import {
CodeExpiredKratosError,
EmailAlreadyExistsError,
IncompatibleSchemaUpgradeError,
InvalidIdentitySessionKratosError,
UnknownKratosError,
} from "@/domain/kratos"
import { checkedToEmailAddress } from "@/domain/users"
import { wrapAsyncFunctionsToRunInSpan } from "@/services/tracing"

// login with email

Expand All @@ -47,7 +48,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe

return data.id as EmailFlowId
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand Down Expand Up @@ -158,7 +159,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe
email = identity.data.recovery_addresses?.[0].value as EmailAddress
totpRequired = true
} else {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand All @@ -168,7 +169,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe
if (isAxiosError(err) && err.response?.status === 403) {
return new CodeExpiredKratosError()
}
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand All @@ -183,7 +184,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe
// we are assuming that email are unique, therefore only one entry can be returned
return identity.data[0]?.verifiable_addresses?.[0].verified ?? false
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand Down Expand Up @@ -211,7 +212,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe

return { authToken, kratosUserId }
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand All @@ -233,7 +234,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe
;({ data: identity } = await kratosAdmin.getIdentity({ id: kratosUserId }))
} catch (err) {
if (!isAxiosError(err)) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}

if (err.message === "Request failed with status code 400") {
Expand Down Expand Up @@ -279,7 +280,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe
}
}

return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand All @@ -289,7 +290,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe
try {
;({ data: identity } = await kratosAdmin.getIdentity({ id: kratosUserId }))
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}

if (identity.schema_id !== SchemaIdType.PhoneEmailNoPasswordV0) {
Expand Down Expand Up @@ -319,7 +320,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe

return email
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand All @@ -329,7 +330,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe
try {
;({ data: identity } = await kratosAdmin.getIdentity({ id: kratosUserId }))
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}

if (identity.schema_id !== "phone_email_no_password_v0") {
Expand Down Expand Up @@ -363,7 +364,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe

return phone
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand All @@ -379,7 +380,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe
try {
;({ data: identity } = await kratosAdmin.getIdentity({ id: userId }))
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}

if (identity.schema_id !== SchemaIdType.EmailNoPasswordV0) {
Expand Down Expand Up @@ -408,7 +409,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe

return toDomainIdentityEmailPhone(newIdentity)
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand All @@ -418,7 +419,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe
try {
;({ data: identity } = await kratosAdmin.getIdentity({ id: kratosUserId }))
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}

return !!identity.traits.email
Expand Down
31 changes: 16 additions & 15 deletions core/api/src/services/kratos/auth-phone-no-password.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { CreateIdentityBody, UpdateIdentityBody } from "@ory/client"

import {
AuthenticationKratosError,
IncompatibleSchemaUpgradeError,
KratosError,
UnknownKratosError,
} from "./errors"

import { kratosAdmin, kratosPublic, toDomainIdentityPhone } from "./private"

import { SchemaIdType } from "./schema"

import { handleKratosErrors } from "./errors"

import { KRATOS_MASTER_USER_PASSWORD } from "@/config"

import {
LikelyNoUserWithThisPhoneExistError,
LikelyUserAlreadyExistError,
} from "@/domain/authentication/errors"

import {
AuthenticationKratosError,
IncompatibleSchemaUpgradeError,
KratosError,
UnknownKratosError,
} from "@/domain/kratos"
import { wrapAsyncFunctionsToRunInSpan } from "@/services/tracing"

// login with phone
Expand Down Expand Up @@ -58,7 +59,7 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe
return new AuthenticationKratosError(err.message || err)
}

return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand All @@ -70,7 +71,7 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe
try {
await kratosAdmin.disableSession({ id: sessionId })
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand Down Expand Up @@ -103,7 +104,7 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe
return new LikelyUserAlreadyExistError(err.message || err)
}

return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand All @@ -119,7 +120,7 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe
try {
;({ data: identity } = await kratosAdmin.getIdentity({ id: userId }))
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}

if (identity.schema_id !== "username_password_deviceid_v0") {
Expand All @@ -146,7 +147,7 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe

return toDomainIdentityPhone(newIdentity)
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand All @@ -172,7 +173,7 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe
if (err instanceof Error && err.message === "Request failed with status code 400") {
return new LikelyUserAlreadyExistError(err.message || err)
}
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand All @@ -188,7 +189,7 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe
try {
;({ data: identity } = await kratosAdmin.getIdentity({ id: kratosUserId }))
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}

if (identity.state === undefined) {
Expand All @@ -210,7 +211,7 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe
})
return toDomainIdentityPhone(newIdentity)
} catch (err) {
return new UnknownKratosError(err)
return handleKratosErrors(err)
}
}

Expand Down
Loading

0 comments on commit 5f25b73

Please sign in to comment.