Skip to content

Commit

Permalink
Add dynamic token refresh and authentication error handling (#911)
Browse files Browse the repository at this point in the history
This commit replaces static authentication tokens with a dynamic token injection
interface, improving auth lifecycle management.

Key features include:

1. Dynamic Token Management
   - New authTokenInjector function replaces static token configuration
   - Automatic token refresh on 401 (Unauthorized) responses
   - Proper error handling for 403 (Forbidden) responses

2. Authentication Error Handling
   - Added auth-error event subscription for monitoring
   - Automatic retry for syncLoop and watchLoop operations
   - Manual retry for activate/deactivate/attach/detach operations

Example usage:
  const client = new yorkie.Client(YorkieAPIAddr, {
    apiKey,
    authTokenInjector: async (reason) => {
      if (reason === 'token expired') {
        return await refreshAccessToken();
      }
      return accessToken;
    },
  });

Related to yorkie-team/yorkie#1037

---------

Co-authored-by: Youngteac Hong <[email protected]>
  • Loading branch information
chacha912 and hackerwins authored Nov 7, 2024
1 parent 924ba4c commit cdde945
Show file tree
Hide file tree
Showing 17 changed files with 3,538 additions and 6,757 deletions.
2 changes: 2 additions & 0 deletions docker/docker-compose-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ services:
- '8081:8081'
depends_on:
- mongo
extra_hosts:
- 'host.docker.internal:host-gateway'
mongo:
image: 'mongo:latest'
container_name: 'mongo'
Expand Down
2 changes: 2 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ services:
ports:
- '8080:8080'
- '8081:8081'
extra_hosts:
- 'host.docker.internal:host-gateway'
5 changes: 4 additions & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,16 @@
"@bufbuild/buf": "^1.28.1",
"@bufbuild/protoc-gen-es": "^1.6.0",
"@connectrpc/protoc-gen-connect-es": "^1.4.0",
"@types/express": "^4.17.21",
"@types/google-protobuf": "^3.15.5",
"@types/long": "^4.0.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitest/coverage-istanbul": "^0.34.5",
"@vitest/coverage-v8": "^0.34.5",
"axios": "^1.7.7",
"eslint-plugin-tsdoc": "^0.2.16",
"express": "^4.21.1",
"prettier": "^2.7.1",
"ts-node": "^10.9.1",
"typedoc": "^0.25.13",
Expand All @@ -61,7 +64,7 @@
"vite": "^5.0.12",
"vite-plugin-commonjs": "^0.10.1",
"vite-plugin-dts": "^3.9.1",
"vite-tsconfig-paths": "^4.2.1",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^0.34.5"
},
"dependencies": {
Expand Down
25 changes: 17 additions & 8 deletions packages/sdk/src/api/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,19 +804,28 @@ function toChangePack(pack: ChangePack<Indexable>): PbChangePack {
}

/**
* `errorCodeOf` returns the error code of the given connect error.
* `errorMetadataOf` returns the error metadata of the given connect error.
*/
export function errorCodeOf(error: ConnectError): string {
// NOTE(hackerwins): Currently, we only use the first detail to represent the
// error code.
export function errorMetadataOf(error: ConnectError): Record<string, string> {
if (!(error instanceof ConnectError)) {
return {};
}

// NOTE(chacha912): Currently, we only use the first detail to represent the
// error metadata.
const infos = error.findDetails(ErrorInfo);
for (const info of infos) {
if (info.metadata.code) {
return info.metadata.code;
}
return info.metadata;
}

return '';
return {};
}

/**
* `errorCodeOf` returns the error code of the given connect error.
*/
export function errorCodeOf(error: ConnectError): string {
return errorMetadataOf(error).code ?? '';
}

/**
Expand Down
22 changes: 18 additions & 4 deletions packages/sdk/src/client/auth_interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,28 @@ import { Interceptor } from '@connectrpc/connect';
export function createAuthInterceptor(
apiKey?: string,
token?: string,
): Interceptor {
return (next) => async (req) => {
): {
authInterceptor: Interceptor;
setToken: (token: string) => void;
} {
let currentToken = token;

const setToken = (token: string) => {
currentToken = token;
};

const authInterceptor: Interceptor = (next) => async (req) => {
if (apiKey) {
req.header.set('x-api-key', apiKey);
}
if (token) {
req.header.set('authorization', token);
if (currentToken) {
req.header.set('authorization', currentToken);
}
return await next(req);
};

return {
authInterceptor,
setToken,
};
}
Loading

0 comments on commit cdde945

Please sign in to comment.