@commercetools/checkout-payments-processor-sdk 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +42 -0
- package/biome.json +38 -0
- package/package.json +48 -0
- package/src/api/context/request-context.helper.test.ts +98 -0
- package/src/api/context/request-context.helper.ts +89 -0
- package/src/api/context/request-context.provider.ts +27 -0
- package/src/api/context/types/request-context.type.ts +18 -0
- package/src/api/handlers/payment-components.handler.test.ts +378 -0
- package/src/api/handlers/payment-components.handler.ts +80 -0
- package/src/api/handlers/payment-intents.handler.test.ts +475 -0
- package/src/api/handlers/payment-intents.handler.ts +169 -0
- package/src/api/handlers/status.handler.test.ts +551 -0
- package/src/api/handlers/status.handler.ts +216 -0
- package/src/api/handlers/transaction.handler.ts +86 -0
- package/src/api/handlers/types/handler.type.ts +49 -0
- package/src/api/hooks/authorize.hook.test.ts +62 -0
- package/src/api/hooks/authorize.hook.ts +51 -0
- package/src/api/hooks/jwt-auth.hook.test.ts +54 -0
- package/src/api/hooks/jwt-auth.hook.ts +56 -0
- package/src/api/hooks/oauth2-auth.hook.test.ts +173 -0
- package/src/api/hooks/oauth2-auth.hook.ts +46 -0
- package/src/api/hooks/session-header-auth.hook.ts +47 -0
- package/src/api/hooks/session-query-param-auth.hook.ts +46 -0
- package/src/api/hooks/types/hook.type.ts +12 -0
- package/src/api/index.ts +11 -0
- package/src/api/types.ts +3 -0
- package/src/commercetools/api/base-api.ts +28 -0
- package/src/commercetools/api/cart-api.test.ts +95 -0
- package/src/commercetools/api/cart-api.ts +55 -0
- package/src/commercetools/api/custom-type-api.ts +39 -0
- package/src/commercetools/api/order-api.ts +22 -0
- package/src/commercetools/api/payment-api.ts +64 -0
- package/src/commercetools/api/payment-method-api.ts +118 -0
- package/src/commercetools/api/root-api.ts +109 -0
- package/src/commercetools/errors/ct-api.error.ts +27 -0
- package/src/commercetools/helpers/currency.converter.test.ts +121 -0
- package/src/commercetools/helpers/currency.converter.test_data.json +2379 -0
- package/src/commercetools/helpers/currency.converter.ts +102 -0
- package/src/commercetools/helpers/taxrate.converter.test.ts +48 -0
- package/src/commercetools/helpers/taxrate.converter.ts +39 -0
- package/src/commercetools/index.ts +22 -0
- package/src/commercetools/services/ct-cart.service.test.ts +1616 -0
- package/src/commercetools/services/ct-cart.service.ts +316 -0
- package/src/commercetools/services/ct-custom-type.service.ts +62 -0
- package/src/commercetools/services/ct-order.service.test.ts +136 -0
- package/src/commercetools/services/ct-order.service.ts +47 -0
- package/src/commercetools/services/ct-payment-method.service.test.ts +878 -0
- package/src/commercetools/services/ct-payment-method.service.ts +223 -0
- package/src/commercetools/services/ct-payment.service.test.ts +1123 -0
- package/src/commercetools/services/ct-payment.service.ts +574 -0
- package/src/commercetools/types/api.type.ts +162 -0
- package/src/commercetools/types/cart.type.ts +83 -0
- package/src/commercetools/types/custom-type.type.ts +21 -0
- package/src/commercetools/types/order.type.ts +19 -0
- package/src/commercetools/types/payment-method.type.ts +130 -0
- package/src/commercetools/types/payment.type.ts +74 -0
- package/src/commercetools/types/predefined-custom-types.type.ts +240 -0
- package/src/commercetools/types.ts +7 -0
- package/src/errorx/errorx.ts +625 -0
- package/src/errorx/index.ts +1 -0
- package/src/errorx/types.ts +5 -0
- package/src/fetch/decorators/base.decorator.ts +13 -0
- package/src/fetch/decorators/monitoring.decorator.ts +41 -0
- package/src/fetch/fetch.test.ts +57 -0
- package/src/fetch/index.ts +3 -0
- package/src/fetch/types/fetch.type.ts +11 -0
- package/src/fetch/types.ts +1 -0
- package/src/index.ts +15 -0
- package/src/logger/commercetools-logger.ts +69 -0
- package/src/logger/index.ts +1 -0
- package/src/logger/types/logger.type.ts +6 -0
- package/src/logger/types.ts +1 -0
- package/src/mocks/auth.mock.ts +6 -0
- package/src/mocks/cart.mock.ts +239 -0
- package/src/mocks/ct-api-error.mock.ts +23 -0
- package/src/mocks/index.ts +4 -0
- package/src/mocks/payment.mock.ts +35 -0
- package/src/payment-processor.ts +317 -0
- package/src/security/authn/authns.ts +219 -0
- package/src/security/authn/bearer-utils.test.ts +35 -0
- package/src/security/authn/bearer-utils.ts +28 -0
- package/src/security/authn/jwt-authn-manager.ts +69 -0
- package/src/security/authn/oauth2-authn-manager.ts +105 -0
- package/src/security/authn/session-header-authn-manager.ts +58 -0
- package/src/security/authn/session-query-param-authn-manager.ts +53 -0
- package/src/security/authn/types/authn.type.ts +50 -0
- package/src/security/authz/authorization-manager.ts +39 -0
- package/src/security/authz/types/authz.type.ts +13 -0
- package/src/security/index.ts +13 -0
- package/src/security/services/authorization.service.ts +54 -0
- package/src/security/services/jwt.service.test.ts +27 -0
- package/src/security/services/jwt.service.ts +45 -0
- package/src/security/services/oauth2.service.ts +60 -0
- package/src/security/services/session.service.ts +141 -0
- package/src/security/services/types/authorization.type.ts +10 -0
- package/src/security/services/types/jwt.type.ts +3 -0
- package/src/security/services/types/oauth2.type.ts +15 -0
- package/src/security/services/types/session.type.ts +38 -0
- package/src/security/types.ts +6 -0
- package/tsconfig.json +28 -0
- package/tsconfig.prod.json +9 -0
- package/typedoc.json +6 -0
- package/vitest.config.ts +15 -0
- package/wiki/getting-started.md +13 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ErrorAuthErrorResponse } from '@errorx';
|
|
2
|
+
import type { Logger } from '@logger-types';
|
|
3
|
+
import type { AuthenticationManager, Oauth2Service } from '@security-types';
|
|
4
|
+
import {
|
|
5
|
+
type HeaderBasedAuthentication,
|
|
6
|
+
Oauth2Authentication,
|
|
7
|
+
} from '@/security/authn/authns.js';
|
|
8
|
+
import { validateBearerAuthorization } from '@/security/authn/bearer-utils.js';
|
|
9
|
+
|
|
10
|
+
interface PermissionInfo {
|
|
11
|
+
permission: string;
|
|
12
|
+
principal: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Oauth2AuthenticationManager is a class that implements the AuthenticationManager interface.
|
|
17
|
+
* It is used to authenticate via a specific OAuth2 token.
|
|
18
|
+
* @param opts - The options for the Oauth2AuthenticationManager.
|
|
19
|
+
* @returns A Oauth2AuthenticationManager instance.
|
|
20
|
+
*/
|
|
21
|
+
export class Oauth2AuthenticationManager implements AuthenticationManager {
|
|
22
|
+
private oauth2Service: Oauth2Service;
|
|
23
|
+
private clientId: string;
|
|
24
|
+
private clientSecret: string;
|
|
25
|
+
private authUrl: string;
|
|
26
|
+
private logger: Logger;
|
|
27
|
+
|
|
28
|
+
constructor(opts: {
|
|
29
|
+
oauth2Service: Oauth2Service;
|
|
30
|
+
clientId: string;
|
|
31
|
+
clientSecret: string;
|
|
32
|
+
authUrl: string;
|
|
33
|
+
logger: Logger;
|
|
34
|
+
}) {
|
|
35
|
+
this.oauth2Service = opts.oauth2Service;
|
|
36
|
+
this.clientId = opts.clientId;
|
|
37
|
+
this.clientSecret = opts.clientSecret;
|
|
38
|
+
this.authUrl = opts.authUrl;
|
|
39
|
+
this.logger = opts.logger;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async authenticate(
|
|
43
|
+
authentication: HeaderBasedAuthentication,
|
|
44
|
+
): Promise<Oauth2Authentication> {
|
|
45
|
+
const principal = authentication.getPrincipal();
|
|
46
|
+
const authorizationHeader = principal.authHeader;
|
|
47
|
+
|
|
48
|
+
const token = validateBearerAuthorization(authorizationHeader);
|
|
49
|
+
|
|
50
|
+
const tokenIntrospectionResponseData =
|
|
51
|
+
await this.oauth2Service.introspectToken({
|
|
52
|
+
url: `${this.authUrl}/oauth/introspect`,
|
|
53
|
+
clientId: this.clientId,
|
|
54
|
+
clientSecret: this.clientSecret,
|
|
55
|
+
token,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!tokenIntrospectionResponseData.active) {
|
|
59
|
+
throw new ErrorAuthErrorResponse('invalid_token', {
|
|
60
|
+
skipLog: true,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const scopes = tokenIntrospectionResponseData.scope?.split(' ') ?? null;
|
|
65
|
+
if (!scopes) {
|
|
66
|
+
throw new ErrorAuthErrorResponse('Token has no scopes.', {
|
|
67
|
+
skipLog: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Search for customer_id:<customer> scope
|
|
72
|
+
const customerPermission = this.searchPermission(scopes, 'customer_id');
|
|
73
|
+
|
|
74
|
+
// Search for anonymous_id:<anonymous> scope
|
|
75
|
+
const anonymousPermission = this.searchPermission(scopes, 'anonymous_id');
|
|
76
|
+
|
|
77
|
+
return new Oauth2Authentication(token, {
|
|
78
|
+
scope: tokenIntrospectionResponseData.scope,
|
|
79
|
+
clientId: tokenIntrospectionResponseData.client_id,
|
|
80
|
+
customerId: customerPermission?.principal,
|
|
81
|
+
anonymousId: anonymousPermission?.principal,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private searchPermission(
|
|
86
|
+
scopes: string[],
|
|
87
|
+
...permissions: string[]
|
|
88
|
+
): PermissionInfo | undefined {
|
|
89
|
+
for (const permission of permissions) {
|
|
90
|
+
// Search for customer_id:<customer> scope
|
|
91
|
+
const permissionIndex = scopes.findIndex((element) =>
|
|
92
|
+
element.startsWith(`${permission}`),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (permissionIndex >= 0) {
|
|
96
|
+
const splitPermission = scopes[permissionIndex].split(':');
|
|
97
|
+
return {
|
|
98
|
+
permission: splitPermission[0],
|
|
99
|
+
principal: splitPermission[1],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ErrorAuthErrorResponse } from '@errorx';
|
|
2
|
+
import type { Logger } from '@logger-types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type HeaderBasedAuthentication,
|
|
6
|
+
SessionAuthentication,
|
|
7
|
+
} from '@/security/authn/authns.js';
|
|
8
|
+
import type { AuthenticationManager } from '@/security/authn/types/authn.type.js';
|
|
9
|
+
import type { SessionService } from '@/security/services/types/session.type.js';
|
|
10
|
+
|
|
11
|
+
export class SessionHeaderAuthenticationManager
|
|
12
|
+
implements AuthenticationManager
|
|
13
|
+
{
|
|
14
|
+
private sessionService: SessionService;
|
|
15
|
+
private logger: Logger;
|
|
16
|
+
|
|
17
|
+
constructor(opts: {
|
|
18
|
+
sessionService: SessionService;
|
|
19
|
+
logger: Logger;
|
|
20
|
+
}) {
|
|
21
|
+
this.sessionService = opts.sessionService;
|
|
22
|
+
this.logger = opts.logger;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async authenticate(
|
|
26
|
+
authentication: HeaderBasedAuthentication,
|
|
27
|
+
): Promise<SessionAuthentication> {
|
|
28
|
+
const principal = authentication.getPrincipal();
|
|
29
|
+
try {
|
|
30
|
+
const session = await this.sessionService.verifySession(
|
|
31
|
+
principal.authHeader,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return new SessionAuthentication(principal.authHeader, {
|
|
35
|
+
cartId: this.sessionService.getCartFromSession(session),
|
|
36
|
+
allowedPaymentMethods:
|
|
37
|
+
this.sessionService.getAllowedPaymentMethodsFromSession(session),
|
|
38
|
+
processorUrl: this.sessionService.getProcessorUrlFromSession(session),
|
|
39
|
+
paymentInterface:
|
|
40
|
+
this.sessionService.getPaymentInterfaceFromSession(session),
|
|
41
|
+
checkoutTransactionItemId:
|
|
42
|
+
this.sessionService.getCheckoutTransactionItemIdFromSession(session),
|
|
43
|
+
merchantReturnUrl:
|
|
44
|
+
this.sessionService.getMerchantReturnUrlFromSession(session),
|
|
45
|
+
futureOrderNumber:
|
|
46
|
+
this.sessionService.getFutureOrderNumberFromSession(session),
|
|
47
|
+
giftCardPlannedAmount:
|
|
48
|
+
this.sessionService.getGiftCardPlannedAmountFromSession(session),
|
|
49
|
+
correlationId: this.sessionService.getCorrelationIdFromSession(session),
|
|
50
|
+
});
|
|
51
|
+
} catch (e) {
|
|
52
|
+
this.logger.error({ err: e }, 'Failed to authenticate session');
|
|
53
|
+
throw new ErrorAuthErrorResponse('Session is not active', {
|
|
54
|
+
cause: e as Error,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ErrorAuthErrorResponse } from '@errorx';
|
|
2
|
+
import type { Logger } from '@logger-types';
|
|
3
|
+
import type { AuthenticationManager, SessionService } from '@security-types';
|
|
4
|
+
import {
|
|
5
|
+
type QueryParamBasedAuthentication,
|
|
6
|
+
SessionAuthentication,
|
|
7
|
+
} from '@/security/authn/authns.js';
|
|
8
|
+
|
|
9
|
+
export class SessionQueryParamAuthenticationManager
|
|
10
|
+
implements AuthenticationManager
|
|
11
|
+
{
|
|
12
|
+
private sessionService: SessionService;
|
|
13
|
+
private logger: Logger;
|
|
14
|
+
|
|
15
|
+
constructor(opts: {
|
|
16
|
+
sessionService: SessionService;
|
|
17
|
+
logger: Logger;
|
|
18
|
+
}) {
|
|
19
|
+
this.sessionService = opts.sessionService;
|
|
20
|
+
this.logger = opts.logger;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async authenticate(
|
|
24
|
+
authentication: QueryParamBasedAuthentication,
|
|
25
|
+
): Promise<SessionAuthentication> {
|
|
26
|
+
const principal = authentication.getPrincipal();
|
|
27
|
+
try {
|
|
28
|
+
const session = await this.sessionService.verifySession(
|
|
29
|
+
principal.authQueryParam,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return new SessionAuthentication(principal.authQueryParam, {
|
|
33
|
+
cartId: this.sessionService.getCartFromSession(session),
|
|
34
|
+
allowedPaymentMethods:
|
|
35
|
+
this.sessionService.getAllowedPaymentMethodsFromSession(session),
|
|
36
|
+
processorUrl: this.sessionService.getProcessorUrlFromSession(session),
|
|
37
|
+
paymentInterface:
|
|
38
|
+
this.sessionService.getPaymentInterfaceFromSession(session),
|
|
39
|
+
checkoutTransactionItemId:
|
|
40
|
+
this.sessionService.getCheckoutTransactionItemIdFromSession(session),
|
|
41
|
+
merchantReturnUrl:
|
|
42
|
+
this.sessionService.getMerchantReturnUrlFromSession(session),
|
|
43
|
+
futureOrderNumber:
|
|
44
|
+
this.sessionService.getFutureOrderNumberFromSession(session),
|
|
45
|
+
});
|
|
46
|
+
} catch (e) {
|
|
47
|
+
this.logger.error({ err: e }, 'Failed to authenticate session');
|
|
48
|
+
throw new ErrorAuthErrorResponse('Session is not active', {
|
|
49
|
+
cause: e as Error,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Money } from '@commercetools/platform-sdk';
|
|
2
|
+
|
|
3
|
+
export interface AuthenticationManager {
|
|
4
|
+
authenticate(
|
|
5
|
+
authentication: Authentication,
|
|
6
|
+
): Promise<Authentication> | Authentication;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Authentication<Principal = unknown, Credentials = unknown> {
|
|
10
|
+
hasPrincipal(): boolean;
|
|
11
|
+
getAuthorities(): string[];
|
|
12
|
+
hasCredentials(): boolean;
|
|
13
|
+
getPrincipal(): Principal;
|
|
14
|
+
getCredentials(): Credentials;
|
|
15
|
+
isAuthenticated(): boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type HeaderPrincipal = {
|
|
19
|
+
authHeader: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type QueryParamPrincipal = {
|
|
23
|
+
authQueryParam: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type SessionPrincipal = {
|
|
27
|
+
cartId: string;
|
|
28
|
+
allowedPaymentMethods: string[];
|
|
29
|
+
processorUrl: string;
|
|
30
|
+
/**
|
|
31
|
+
* @deprecated use checkoutTransactionItemId instead
|
|
32
|
+
*/
|
|
33
|
+
paymentInterface?: string;
|
|
34
|
+
checkoutTransactionItemId?: string;
|
|
35
|
+
merchantReturnUrl?: string;
|
|
36
|
+
futureOrderNumber?: string;
|
|
37
|
+
giftCardPlannedAmount?: Money;
|
|
38
|
+
correlationId?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type Oauth2Principal = {
|
|
42
|
+
clientId: string;
|
|
43
|
+
scope: string;
|
|
44
|
+
customerId?: string;
|
|
45
|
+
anonymousId?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type JWTPrincipal = {
|
|
49
|
+
mcCustomerId?: string;
|
|
50
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ErrorAuthErrorResponse } from '@errorx';
|
|
2
|
+
import type { Logger } from '@logger-types';
|
|
3
|
+
import type { Authentication, AuthorizationManager } from '@security-types';
|
|
4
|
+
|
|
5
|
+
export class AuthorityAuthorizationManager
|
|
6
|
+
implements AuthorizationManager<string[]>
|
|
7
|
+
{
|
|
8
|
+
private logger: Logger;
|
|
9
|
+
|
|
10
|
+
constructor(opts: { logger: Logger }) {
|
|
11
|
+
this.logger = opts.logger;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
verify(authentication: Authentication, authorities: string[]) {
|
|
15
|
+
const isAuthorized = this.check(authentication, authorities);
|
|
16
|
+
if (!isAuthorized) {
|
|
17
|
+
throw new ErrorAuthErrorResponse('Not authorized', {
|
|
18
|
+
skipLog: true,
|
|
19
|
+
fields: {
|
|
20
|
+
validAuthorities: authorities,
|
|
21
|
+
},
|
|
22
|
+
privateFields: {
|
|
23
|
+
grantedAuthorities: authentication.getAuthorities(),
|
|
24
|
+
requiredAuthorities: authorities,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
check(authentication: Authentication, authorities: string[]) {
|
|
31
|
+
const grantedAuthorities = authentication.getAuthorities();
|
|
32
|
+
const hasGrantedAuthorities = authorities.some((authority) => {
|
|
33
|
+
return grantedAuthorities.find(
|
|
34
|
+
(grantedAuthority: string) => grantedAuthority === authority,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
return hasGrantedAuthorities;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Authentication } from '@/security/authn/types/authn.type.js';
|
|
2
|
+
|
|
3
|
+
export interface AuthorizationManager<T> {
|
|
4
|
+
/**
|
|
5
|
+
* Determines if access should be granted for a specific authentication and object.
|
|
6
|
+
*/
|
|
7
|
+
verify(authentication: Authentication, object: T): void;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Determines if access is granted for a specific object.
|
|
11
|
+
*/
|
|
12
|
+
check(authentication: Authentication, object: T): boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// authn
|
|
2
|
+
export * from '@/security/authn/authns.js';
|
|
3
|
+
export * from '@/security/authn/jwt-authn-manager.js';
|
|
4
|
+
export * from '@/security/authn/oauth2-authn-manager.js';
|
|
5
|
+
export * from '@/security/authn/session-header-authn-manager.js';
|
|
6
|
+
export * from '@/security/authn/session-query-param-authn-manager.js';
|
|
7
|
+
// authz
|
|
8
|
+
export * from '@/security/authz/authorization-manager.js';
|
|
9
|
+
export * from '@/security/services/authorization.service.js';
|
|
10
|
+
// services
|
|
11
|
+
export * from '@/security/services/jwt.service.js';
|
|
12
|
+
export * from '@/security/services/oauth2.service.js';
|
|
13
|
+
export * from '@/security/services/session.service.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ErrorGeneral } from '@errorx';
|
|
2
|
+
import type { Logger } from '@logger-types';
|
|
3
|
+
import type { Fetch } from '@/fetch/types/fetch.type.js';
|
|
4
|
+
import type {
|
|
5
|
+
AuthorizationService,
|
|
6
|
+
CommercetoolsToken,
|
|
7
|
+
} from '@/security/services/types/authorization.type.js';
|
|
8
|
+
|
|
9
|
+
export class DefaultAuthorizationService implements AuthorizationService {
|
|
10
|
+
private authUrl: string;
|
|
11
|
+
private clientId: string;
|
|
12
|
+
private clientSecret: string;
|
|
13
|
+
private fetch: Fetch;
|
|
14
|
+
private logger: Logger;
|
|
15
|
+
|
|
16
|
+
constructor(opts: {
|
|
17
|
+
authUrl: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
clientSecret: string;
|
|
20
|
+
fetch: Fetch;
|
|
21
|
+
logger: Logger;
|
|
22
|
+
}) {
|
|
23
|
+
this.authUrl = opts.authUrl;
|
|
24
|
+
this.clientId = opts.clientId;
|
|
25
|
+
this.clientSecret = opts.clientSecret;
|
|
26
|
+
this.fetch = opts.fetch;
|
|
27
|
+
this.logger = opts.logger;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getAccessToken(): Promise<CommercetoolsToken> {
|
|
31
|
+
const encodedCredentials = btoa(`${this.clientId}:${this.clientSecret}`);
|
|
32
|
+
const urlencoded = new URLSearchParams();
|
|
33
|
+
urlencoded.append('grant_type', 'client_credentials');
|
|
34
|
+
const response = await this.fetch(`${this.authUrl}/oauth/token`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
38
|
+
Authorization: `Basic ${encodedCredentials}`,
|
|
39
|
+
},
|
|
40
|
+
body: urlencoded,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new ErrorGeneral(undefined, {
|
|
45
|
+
privateMessage: 'Failed to get auth token',
|
|
46
|
+
privateFields: {
|
|
47
|
+
responseStatus: response.status,
|
|
48
|
+
responseText: await response.text(),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return (await response.json()) as CommercetoolsToken;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CommercetoolsLogger } from '@logger';
|
|
2
|
+
import { RequestContextProvider } from '@/api/index.js';
|
|
3
|
+
import { DefaultJWTService } from '@/security/services/jwt.service.js';
|
|
4
|
+
|
|
5
|
+
describe('jwt', () => {
|
|
6
|
+
test('wrong jwt', async () => {
|
|
7
|
+
const projectKey = 'test';
|
|
8
|
+
const contextProvider = new RequestContextProvider({
|
|
9
|
+
getContextFn: () => ({
|
|
10
|
+
correlationId: 'correlation-id',
|
|
11
|
+
requestId: 'request-id',
|
|
12
|
+
}),
|
|
13
|
+
updateContextFn: () => {},
|
|
14
|
+
});
|
|
15
|
+
const logger = new CommercetoolsLogger({ contextProvider, projectKey });
|
|
16
|
+
const jwtService = new DefaultJWTService({
|
|
17
|
+
jwksUrl: `http://localhost:9000/jwt/.well-known/jwks.json`,
|
|
18
|
+
logger,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const verifyPromise = jwtService.verify({
|
|
22
|
+
token: `some.wrong.token`,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await expect(verifyPromise).rejects.toThrow('invalid token');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ErrorAuthErrorResponse } from '@errorx';
|
|
2
|
+
import type { Logger } from '@logger-types';
|
|
3
|
+
import type { JWTService } from '@security-types';
|
|
4
|
+
import jwt, { type JwtHeader, type SigningKeyCallback } from 'jsonwebtoken';
|
|
5
|
+
import jwksClient from 'jwks-rsa';
|
|
6
|
+
|
|
7
|
+
export class DefaultJWTService implements JWTService {
|
|
8
|
+
private client: jwksClient.JwksClient;
|
|
9
|
+
private logger: Logger;
|
|
10
|
+
|
|
11
|
+
constructor(opts: { jwksUrl: string; logger: Logger }) {
|
|
12
|
+
this.client = jwksClient({
|
|
13
|
+
jwksUri: opts.jwksUrl,
|
|
14
|
+
});
|
|
15
|
+
this.logger = opts.logger;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async verify(opts: { token: string | undefined }) {
|
|
19
|
+
const getKey = (header: JwtHeader, callback: SigningKeyCallback) => {
|
|
20
|
+
this.client.getSigningKey(header.kid, (err, key) => {
|
|
21
|
+
if (err) {
|
|
22
|
+
return callback(err);
|
|
23
|
+
}
|
|
24
|
+
const signingKey = key?.getPublicKey();
|
|
25
|
+
callback(null, signingKey);
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
if (!opts.token) {
|
|
30
|
+
throw new ErrorAuthErrorResponse('Token is missing');
|
|
31
|
+
}
|
|
32
|
+
jwt.verify(opts.token, getKey, {}, (err, decoded) => {
|
|
33
|
+
if (err) {
|
|
34
|
+
return reject(
|
|
35
|
+
new ErrorAuthErrorResponse(err.message, {
|
|
36
|
+
privateMessage: err.message,
|
|
37
|
+
cause: err,
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return resolve(decoded);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ErrorAuthErrorResponse, ErrorGeneral } from '@errorx';
|
|
2
|
+
import type { Logger } from '@logger-types';
|
|
3
|
+
import type { Oauth2Service, TokenInfo } from '@security-types';
|
|
4
|
+
|
|
5
|
+
export class DefaultOauth2Service implements Oauth2Service {
|
|
6
|
+
private logger: Logger;
|
|
7
|
+
constructor(opts: { logger: Logger }) {
|
|
8
|
+
this.logger = opts.logger;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async introspectToken(opts: {
|
|
12
|
+
url: string;
|
|
13
|
+
clientId: string;
|
|
14
|
+
clientSecret: string;
|
|
15
|
+
token: string;
|
|
16
|
+
}) {
|
|
17
|
+
const urlencoded = new URLSearchParams();
|
|
18
|
+
urlencoded.append('token', opts.token);
|
|
19
|
+
|
|
20
|
+
const tokenResponse = await fetch(opts.url, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
24
|
+
Authorization: `Basic ${btoa(`${opts.clientId}:${opts.clientSecret}`)}`,
|
|
25
|
+
},
|
|
26
|
+
body: urlencoded,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (tokenResponse.status > 299) {
|
|
30
|
+
if (tokenResponse.status === 401) {
|
|
31
|
+
const tokenResponseJson = (await tokenResponse.json()) as {
|
|
32
|
+
message: string;
|
|
33
|
+
error: string;
|
|
34
|
+
};
|
|
35
|
+
throw new ErrorAuthErrorResponse(
|
|
36
|
+
tokenResponseJson.message,
|
|
37
|
+
{
|
|
38
|
+
privateFields: {
|
|
39
|
+
clientId: opts.clientId,
|
|
40
|
+
status: tokenResponse.status,
|
|
41
|
+
},
|
|
42
|
+
skipLog: true,
|
|
43
|
+
},
|
|
44
|
+
tokenResponseJson.error,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new ErrorGeneral('Failed to authorize request.', {
|
|
49
|
+
privateMessage: 'some error happened while requesting token from coco',
|
|
50
|
+
privateFields: {
|
|
51
|
+
clientId: opts.clientId,
|
|
52
|
+
status: tokenResponse.status,
|
|
53
|
+
},
|
|
54
|
+
skipLog: true,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (await tokenResponse.json()) as TokenInfo;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Money } from '@commercetools/platform-sdk';
|
|
2
|
+
import { ErrorAuthErrorResponse, ErrorGeneral } from '@errorx';
|
|
3
|
+
import type { Logger } from '@logger-types';
|
|
4
|
+
import type {
|
|
5
|
+
AuthorizationService,
|
|
6
|
+
CommercetoolsToken,
|
|
7
|
+
Session,
|
|
8
|
+
SessionService,
|
|
9
|
+
} from '@security-types';
|
|
10
|
+
|
|
11
|
+
export class DefaultSessionService implements SessionService {
|
|
12
|
+
private authorizationService: AuthorizationService;
|
|
13
|
+
private sessionUrl: string;
|
|
14
|
+
private projectKey: string;
|
|
15
|
+
protected token: CommercetoolsToken | undefined;
|
|
16
|
+
private logger: Logger;
|
|
17
|
+
|
|
18
|
+
constructor(opts: {
|
|
19
|
+
authorizationService: AuthorizationService;
|
|
20
|
+
sessionUrl: string;
|
|
21
|
+
projectKey: string;
|
|
22
|
+
logger: Logger;
|
|
23
|
+
}) {
|
|
24
|
+
this.authorizationService = opts.authorizationService;
|
|
25
|
+
this.sessionUrl = opts.sessionUrl;
|
|
26
|
+
this.projectKey = opts.projectKey;
|
|
27
|
+
this.logger = opts.logger;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async verifySession(sessionId: string): Promise<Session> {
|
|
31
|
+
if (!this.token) {
|
|
32
|
+
this.token = await this.authorizationService.getAccessToken();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let res = await this.getSession(sessionId);
|
|
36
|
+
// If commercetools oauth token is expired, get a new one and retry
|
|
37
|
+
if (res.status === 401 || res.status === 403) {
|
|
38
|
+
this.token = await this.authorizationService.getAccessToken();
|
|
39
|
+
res = await this.getSession(sessionId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (res.status === 404) {
|
|
43
|
+
throw new ErrorAuthErrorResponse('commercetools session not found', {
|
|
44
|
+
privateFields: {
|
|
45
|
+
status: res.status,
|
|
46
|
+
statusText: res.statusText,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new ErrorGeneral('Could not get commercetools session', {
|
|
53
|
+
privateFields: {
|
|
54
|
+
status: res.status,
|
|
55
|
+
statusText: res.statusText,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const session = (await res.json()) as Session;
|
|
61
|
+
if (session.state !== 'ACTIVE') {
|
|
62
|
+
throw new ErrorAuthErrorResponse(
|
|
63
|
+
`commercetools session is not ACTIVE, current status: ${session.state}`,
|
|
64
|
+
{
|
|
65
|
+
privateFields: {
|
|
66
|
+
session: JSON.stringify(session),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return session;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getCartFromSession(session: Session): string {
|
|
75
|
+
if (!session.activeCart?.cartRef?.id) {
|
|
76
|
+
throw new ErrorAuthErrorResponse(
|
|
77
|
+
'Cart not found in commercetools session',
|
|
78
|
+
{
|
|
79
|
+
privateFields: {
|
|
80
|
+
session: JSON.stringify(session),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return session.activeCart.cartRef.id;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getAllowedPaymentMethodsFromSession(session: Session): string[] {
|
|
89
|
+
return (session.metadata?.allowedPaymentMethods as string[]) || [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getProcessorUrlFromSession(session: Session): string {
|
|
93
|
+
return session.metadata?.processorUrl as string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getPaymentInterfaceFromSession(session: Session): string | undefined {
|
|
97
|
+
return session.metadata?.paymentInterface as string | undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getCheckoutTransactionItemIdFromSession(
|
|
101
|
+
session: Session,
|
|
102
|
+
): string | undefined {
|
|
103
|
+
return session.metadata?.checkoutTransactionItemId as string | undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getMerchantReturnUrlFromSession(session: Session): string | undefined {
|
|
107
|
+
return session.metadata?.merchantReturnUrl as string | undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getFutureOrderNumberFromSession(session: Session): string | undefined {
|
|
111
|
+
return session.metadata?.futureOrderNumber as string | undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getGiftCardPlannedAmountFromSession(session: Session): Money | undefined {
|
|
115
|
+
return session.metadata?.giftCardPlannedAmount as Money;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getCorrelationIdFromSession(session: Session): string | undefined {
|
|
119
|
+
return session.metadata?.correlationId as string | undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async getSession(sessionId: string) {
|
|
123
|
+
if (!this.token) {
|
|
124
|
+
throw new ErrorAuthErrorResponse('Token not found', {
|
|
125
|
+
privateFields: {
|
|
126
|
+
token: this.token,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return await fetch(
|
|
131
|
+
`${this.sessionUrl}/${this.projectKey}/sessions/${sessionId}`,
|
|
132
|
+
{
|
|
133
|
+
method: 'GET',
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
Authorization: `Bearer ${this.token.access_token}`,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|