Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@atproto/oauth-client

Package Overview
Dependencies
Maintainers
0
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@atproto/oauth-client - npm Package Compare versions

Comparing version 0.1.7 to 0.2.0

dist/oauth-session.d.ts

32

CHANGELOG.md
# @atproto/oauth-client
## 0.2.0
### Minor Changes
- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The `OAuthClient` (and runtime specific sub-classes) no longer return @atproto/api `Agent` instances. Instead, they return `OAuthSession` instances that can be used to instantiate the `Agent` class.
- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove "nonce" from authorization request
- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Mandate the use of "atproto" scope
- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove "openid" compatibility. The reason is that although we were technically "openid" compatible, ATProto identifiers are distributed identifiers. When a client relies on OpenID to authenticate users, it will use the auth provider in combination with the identifier to uniquely identify the user. Since ATProto identifiers are meant to be able to move from one provider to the other, OpenID compatibility could break authentication after a user was migrated to a different provider.
The way OpenID compliant clients would adapt to this particularity would typically be to remove the provider + identifier combination and use the identifier alone. While this is indeed the right way to handle ATProto identifiers, it requires more work to avoid impersonation. In particular, when obtaining a user identifier, the client **must** verify that the issuer of the identity token is indeed the server responsible for that user. This mechanism being not enforced by the OpenID standard, OpenID compatibility could lead to security issues. For this reason, we decided to remove OpenID compatibility from the OAuth provider.
Note that a trusted central authority could still offer OpenID compatibility by relying on ATProto's regular OAuth flow under the hood. This capability is out of the scope of this library.
- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename OAuthAgent into OAuthSession
- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `OAuthSession`'s `request` method to `fetchHandler`. The goal of this change is to allow `OAuthSession` to be used in order to instantiate `XrpcClient` by implementing the `FetchHandlerObject` interface.
### Patch Changes
- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `getTokenInfo()` method to `OAuthSession`.
- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Do not remove scopes not advertised in the AS's "scopes_supported" when building the authorization request.
- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make `getTokenSet()` method public in `OAuthSession`.
- Updated dependencies [[`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]:
- @atproto/xrpc@0.6.1
- @atproto/oauth-types@0.1.4
## 0.1.7

@@ -4,0 +36,0 @@

3

dist/index.d.ts

@@ -6,4 +6,2 @@ export * from '@atproto-labs/did-resolver';

export * from '@atproto/oauth-types';
export * from './oauth-agent.js';
export * from './oauth-atp-agent.js';
export * from './oauth-authorization-server-metadata-resolver.js';

@@ -17,2 +15,3 @@ export * from './oauth-callback-error.js';

export * from './oauth-server-factory.js';
export * from './oauth-session.js';
export * from './runtime-implementation.js';

@@ -19,0 +18,0 @@ export * from './session-getter.js';

@@ -26,4 +26,2 @@ "use strict";

__exportStar(require("@atproto/oauth-types"), exports);
__exportStar(require("./oauth-agent.js"), exports);
__exportStar(require("./oauth-atp-agent.js"), exports);
__exportStar(require("./oauth-authorization-server-metadata-resolver.js"), exports);

@@ -37,2 +35,3 @@ __exportStar(require("./oauth-callback-error.js"), exports);

__exportStar(require("./oauth-server-factory.js"), exports);
__exportStar(require("./oauth-session.js"), exports);
__exportStar(require("./runtime-implementation.js"), exports);

@@ -39,0 +38,0 @@ __exportStar(require("./session-getter.js"), exports);

@@ -7,3 +7,2 @@ import { DidCache } from '@atproto-labs/did-resolver';

import { OAuthClientIdDiscoverable, OAuthClientMetadata, OAuthClientMetadataInput, OAuthResponseMode } from '@atproto/oauth-types';
import { OAuthAtpAgent } from './oauth-atp-agent.js';
import { AuthorizationServerMetadataCache } from './oauth-authorization-server-metadata-resolver.js';

@@ -14,2 +13,3 @@ import { ProtectedResourceMetadataCache } from './oauth-protected-resource-metadata-resolver.js';

import { OAuthServerFactory } from './oauth-server-factory.js';
import { OAuthSession } from './oauth-session.js';
import { RuntimeImplementation } from './runtime-implementation.js';

@@ -60,3 +60,3 @@ import { Runtime } from './runtime.js';

e: string;
alg?: "RS256" | "PS256" | "RS384" | "PS384" | "RS512" | "PS512" | undefined;
alg?: "RS256" | "RS384" | "RS512" | "PS256" | "PS384" | "PS512" | undefined;
kid?: string | undefined;

@@ -192,3 +192,3 @@ ext?: boolean | undefined;

readonly e: string;
readonly alg?: "RS256" | "PS256" | "RS384" | "PS384" | "RS512" | "PS512" | undefined;
readonly alg?: "RS256" | "RS384" | "RS512" | "PS256" | "PS384" | "PS512" | undefined;
readonly kid?: string | undefined;

@@ -293,15 +293,15 @@ readonly ext?: boolean | undefined;

callback(params: URLSearchParams): Promise<{
agent: OAuthAtpAgent;
session: OAuthSession;
state: string | null;
}>;
/**
* Build an agent from a stored session. This will refresh the token only if
* needed (about to expire) by default.
* Load a stored session. This will refresh the token only if needed (about to
* expire) by default.
*
* @param refresh See {@link SessionGetter.getSession}
*/
restore(sub: string, refresh?: boolean): Promise<OAuthAtpAgent>;
restore(sub: string, refresh?: boolean): Promise<OAuthSession>;
revoke(sub: string): Promise<void>;
createAgent(server: OAuthServerAgent, sub: string): OAuthAtpAgent;
protected createSession(server: OAuthServerAgent, sub: string): OAuthSession;
}
//# sourceMappingURL=oauth-client.d.ts.map

@@ -12,4 +12,2 @@ "use strict";

const token_revoked_error_js_1 = require("./errors/token-revoked-error.js");
const oauth_agent_js_1 = require("./oauth-agent.js");
const oauth_atp_agent_js_1 = require("./oauth-atp-agent.js");
const oauth_authorization_server_metadata_resolver_js_1 = require("./oauth-authorization-server-metadata-resolver.js");

@@ -20,2 +18,3 @@ const oauth_callback_error_js_1 = require("./oauth-callback-error.js");

const oauth_server_factory_js_1 = require("./oauth-server-factory.js");
const oauth_session_js_1 = require("./oauth-session.js");
const runtime_js_1 = require("./runtime.js");

@@ -156,3 +155,2 @@ const session_getter_js_1 = require("./session-getter.js");

const { identity, metadata } = await this.oauthResolver.resolve(input, options);
const nonce = await this.runtime.generateNonce();
const pkce = await this.runtime.generatePKCE();

@@ -164,4 +162,3 @@ const dpopKey = await this.runtime.generateKey(metadata.dpop_signing_alg_values_supported || [constants_js_1.FALLBACK_ALG]);

dpopKey,
nonce,
verifier: pkce?.verifier,
verifier: pkce.verifier,
appState: options?.state,

@@ -172,5 +169,4 @@ });

redirect_uri: redirectUri,
code_challenge: pkce?.challenge,
code_challenge_method: pkce?.method,
nonce,
code_challenge: pkce.challenge,
code_challenge_method: pkce.method,
state,

@@ -185,9 +181,4 @@ login_hint: identity

display: options?.display,
id_token_hint: options?.id_token_hint,
max_age: options?.max_age, // this.clientMetadata.default_max_age
prompt: options?.prompt,
scope: options?.scope
?.split(' ')
.filter((s) => metadata.scopes_supported?.includes(s))
.join(' '),
scope: options?.scope || undefined,
ui_locales: options?.ui_locales,

@@ -279,12 +270,8 @@ };

try {
if (tokenSet.id_token) {
await this.runtime.validateIdTokenClaims(tokenSet.id_token, stateParam, stateData.nonce, codeParam, tokenSet.access_token);
}
const { sub } = tokenSet;
await this.sessionGetter.setStored(sub, {
await this.sessionGetter.setStored(tokenSet.sub, {
dpopKey: stateData.dpopKey,
tokenSet,
});
const agent = this.createAgent(server, sub);
return { agent, state: stateData.appState ?? null };
const session = this.createSession(server, tokenSet.sub);
return { session, state: stateData.appState ?? null };
}

@@ -303,4 +290,4 @@ catch (err) {

/**
* Build an agent from a stored session. This will refresh the token only if
* needed (about to expire) by default.
* Load a stored session. This will refresh the token only if needed (about to
* expire) by default.
*

@@ -315,3 +302,3 @@ * @param refresh See {@link SessionGetter.getSession}

});
return this.createAgent(server, sub);
return this.createSession(server, sub);
}

@@ -331,5 +318,4 @@ async revoke(sub) {

}
createAgent(server, sub) {
const oauthAgent = new oauth_agent_js_1.OAuthAgent(server, sub, this.sessionGetter, this.fetch);
return new oauth_atp_agent_js_1.OAuthAtpAgent(oauthAgent);
createSession(server, sub) {
return new oauth_session_js_1.OAuthSession(server, sub, this.sessionGetter, this.fetch);
}

@@ -336,0 +322,0 @@ }

import { Fetch, Json } from '@atproto-labs/fetch';
import { SimpleStore } from '@atproto-labs/simple-store';
import { Key, Keyset, SignedJwt } from '@atproto/jwk';
import { Key, Keyset } from '@atproto/jwk';
import { OAuthAuthorizationServerMetadata, OAuthClientIdentification, OAuthEndpointName, OAuthParResponse, OAuthTokenResponse, OAuthTokenType } from '@atproto/oauth-types';

@@ -12,4 +12,3 @@ import { OAuthResolver } from './oauth-resolver.js';

aud: string;
scope?: string;
id_token?: SignedJwt;
scope: string;
refresh_token?: string;

@@ -16,0 +15,0 @@ access_token: string;

@@ -173,5 +173,11 @@ "use strict";

const { sub } = tokenResponse;
// ATPROTO requires that the "sub" is always present in the token response.
if (!sub)
throw new TypeError(`Missing "sub" in token response`);
if (!sub || typeof sub !== 'string') {
throw new TypeError(`Unexpected ${typeof sub} "sub" in token response`);
}
// Using an array to check for the presence of the "atproto" scope (we don't
// want atproto to be a substring of another scope)
const scopes = tokenResponse.scope?.split(' ');
if (!scopes?.includes('atproto')) {
throw new TypeError('Missing "atproto" scope in token response');
}
// @TODO (?) make timeout configurable

@@ -182,3 +188,3 @@ const signal = __addDisposableResource(env_1, (0, util_js_1.timeoutSignal)(10e3), false);

});
if (resolved.metadata.issuer !== this.serverMetadata.issuer) {
if (this.serverMetadata.issuer !== resolved.metadata.issuer) {
// Best case scenario; the user switched PDS. Worst case scenario; a bad

@@ -190,7 +196,6 @@ // actor is trying to impersonate a user. In any case, we must not allow

return {
sub,
aud: resolved.identity.pds.href,
iss: resolved.metadata.issuer,
sub,
scope: tokenResponse.scope,
id_token: tokenResponse.id_token,
refresh_token: tokenResponse.refresh_token,

@@ -197,0 +202,0 @@ access_token: tokenResponse.access_token,

@@ -1,2 +0,2 @@

import { JwtHeader, JwtPayload, Key } from '@atproto/jwk';
import { Key } from '@atproto/jwk';
import { RuntimeImplementation, RuntimeLock } from './runtime-implementation.js';

@@ -11,11 +11,2 @@ export declare class Runtime {

generateNonce(length?: number): Promise<string>;
validateIdTokenClaims(token: string, state: string, nonce: string, code?: string, accessToken?: string): Promise<{
header: JwtHeader;
payload: JwtPayload;
}>;
private validateHashClaim;
protected generateHashClaim(source: string, header: {
alg: string;
crv?: string;
}): Promise<string>;
generatePKCE(byteLength?: number): Promise<{

@@ -22,0 +13,0 @@ verifier: string;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Runtime = void 0;
const jwk_1 = require("@atproto/jwk");
const base64_1 = require("multiformats/bases/base64");

@@ -47,42 +46,2 @@ const lock_js_1 = require("./lock.js");

}
async validateIdTokenClaims(token, state, nonce, code, accessToken) {
// It's fine to use unsafeDecodeJwt here because the token was received from
// the server's token endpoint. The following checks are to ensure that the
// oauth flow was indeed initiated by the client.
const { header, payload } = (0, jwk_1.unsafeDecodeJwt)(token);
if (!payload.nonce || payload.nonce !== nonce) {
throw new TypeError('Nonce mismatch');
}
if (payload.c_hash) {
await this.validateHashClaim(payload.c_hash, code, header);
}
if (payload.s_hash) {
await this.validateHashClaim(payload.s_hash, state, header);
}
if (payload.at_hash) {
await this.validateHashClaim(payload.at_hash, accessToken, header);
}
return { header, payload };
}
async validateHashClaim(claim, source, header) {
if (typeof claim !== 'string' || !claim) {
throw new TypeError(`string "_hash" claim expected`);
}
if (typeof source !== 'string' || !source) {
throw new TypeError(`string value expected`);
}
const expected = await this.generateHashClaim(source, header);
if (expected !== claim) {
throw new TypeError(`"_hash" does not match`);
}
}
async generateHashClaim(source, header) {
const algo = getHashAlgo(header);
const bytes = new TextEncoder().encode(source);
const digest = await this.implementation.digest(bytes, algo);
if (digest.length % 2 !== 0)
throw new TypeError('Invalid digest length');
const digestHalf = digest.slice(0, digest.length / 2);
return base64_1.base64url.baseEncode(digestHalf);
}
async generatePKCE(byteLength) {

@@ -117,31 +76,2 @@ const verifier = await this.generateVerifier(byteLength);

exports.Runtime = Runtime;
function getHashAlgo(header) {
switch (header.alg) {
case 'HS256':
case 'RS256':
case 'PS256':
case 'ES256':
case 'ES256K':
return { name: 'sha256' };
case 'HS384':
case 'RS384':
case 'PS384':
case 'ES384':
return { name: 'sha384' };
case 'HS512':
case 'RS512':
case 'PS512':
case 'ES512':
return { name: 'sha512' };
case 'EdDSA':
switch (header.crv) {
case 'Ed25519':
return { name: 'sha512' };
default:
throw new TypeError('unrecognized or invalid EdDSA curve provided');
}
default:
throw new TypeError('unrecognized or invalid JWS algorithm provided');
}
}
function extractJktComponents(jwk) {

@@ -148,0 +78,0 @@ const get = (field) => {

@@ -5,3 +5,2 @@ import { SimpleStore } from '@atproto-labs/simple-store';

iss: string;
nonce: string;
dpopKey: Key;

@@ -8,0 +7,0 @@ verifier?: string;

{
"name": "@atproto/oauth-client",
"version": "0.1.7",
"version": "0.2.0",
"license": "MIT",

@@ -36,7 +36,6 @@ "description": "OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).",

"@atproto-labs/simple-store-memory": "0.1.1",
"@atproto/api": "0.13.3",
"@atproto/did": "0.1.1",
"@atproto/jwk": "0.1.1",
"@atproto/oauth-types": "0.1.3",
"@atproto/xrpc": "0.6.0"
"@atproto/oauth-types": "0.1.4",
"@atproto/xrpc": "0.6.1"
},

@@ -43,0 +42,0 @@ "devDependencies": {

# @atproto/oauth-client: atproto flavoured OAuth client
Core library for implementing [ATPROTO] OAuth clients.
Core library for implementing [atproto][ATPROTO] OAuth clients.

@@ -150,4 +150,32 @@ For a browser specific implementation, see [@atproto/oauth-client-browser](https://www.npmjs.com/package/@atproto/oauth-client-browser).

const agent = result.agent
const oauthSession = result.session
```
The sign-in process results in an `OAuthSession` instance that can be used to make
authenticated requests to the resource server. This instance will automatically
refresh the credentials when needed.
### Making authenticated requests
The `OAuthSession` instance obtained after signing in can be used to make
authenticated requests to the user's PDS. There are two main use-cases:
1. Making authenticated request to Bluesky's AppView in order to fetch and
manipulate data from the `app.bsky` lexicon.
2. Making authenticated request to your own AppView, in order to fetch and
manipulate data from your own lexicon.
#### Making authenticated requests to Bluesky's AppView
The `@atproto/oauth-client` package provides a `OAuthSession` class that can be
used to make authenticated requests to Bluesky's AppView. This can be achieved
by constructing an `Agent` (from `@atproto/api`) instance using the
`OAuthSession` instance.
```ts
import { Agent } from '@atproto/api'
const agent = new Agent(oauthSession)
// Make an authenticated request to the server. New credentials will be

@@ -159,8 +187,102 @@ // automatically fetched if needed (causing sessionStore.set() to be called).

if (agent instanceof AtpAgent) {
// revoke credentials on the server (causing sessionStore.del() to be called)
await agent.logout()
}
// revoke credentials on the server (causing sessionStore.del() to be called)
await agent.signOut()
```
#### Making authenticated requests to your own AppView
The `OAuthSession` instance obtained after signing in can be used to instantiate
the `XrpcClient` class from the `@atproto/xrpc` package.
```ts
import { Lexicons } from '@atproto/lexicon'
import { OAuthClient } from '@atproto/oauth-client' // or "@atproto/oauth-client-browser" or "@atproto/oauth-client-node"
import { XrpcClient } from '@atproto/xrpc'
// Define your lexicons
const myLexicon = new Lexicons([
{
lexicon: 1,
id: 'com.example.query',
defs: {
main: {
// ...
},
},
},
])
// Describe your app's oauth client
const oauthClient = new OAuthClient({
// ...
})
// Authenticate the user
const oauthSession = await oauthClient.restore('did:plc:123')
// Instantiate a client using the `oauthSession` as fetch handler object
const client = new XrpcClient(oauthSession, myLexicon)
// Make authenticated calls
const response = await client.call('com.example.query')
```
Note that the user's PDS might not know about your lexicon, or what to do with
those calls (PDS' are only mandated to implement the `com.atproto` lexicon). In
order to process your calls, you need to have a backend that will process those
calls. You can then instruct your PDS to forward those calls to your backend.
```ts
const response = await client.call(
'com.example.query',
{
// Params
},
{
headers: {
// The PDS will proxy calls to the specified service in did:plc:xyz's did document.
// These calls will be authenticated using "service auth", a single use JWT Bearer token, signed with the logged-in user's private key.
'atproto-proxy': 'did:plc:xyz#serviceId',
},
},
)
```
You can also instantiate the `XrpcClient` class with a custom `fetch` function
that will provide the `atproto-proxy` header on all calls:
```ts
const boundClient = new XrpcClient((url, init) => {
const headers = new Headers(init?.headers)
// Add the atproto-proxy header if it is not already present
if (!headers.has('atproto-proxy')) {
headers.set('atproto-proxy', 'did:plc:xyz#serviceId')
}
return oauthSession.fetchHandler(url, { ...init, headers })
}, myLexicon)
// No need to specify the atproto-proxy header anymore
const response = await boundClient.call('com.example.query')
```
> [!NOTE]
>
> Proxying every call through the PDS is not recommended for performance
> reasons, as it will increase the latency of readonly calls to your lexicon.
> Doing so will also prevent your backend from being able to anticipate writes
> on the network. Indeed, write calls will be sent to the PDS, which will then
> propagate them on the network through a relay (a.k.a. "firehose"). This will
> introduce a delay between the time the write is made and the time it is
> processed by your backend.
>
> In order to avoid those issues, it is recommended that you implement your
> backend using a backend-for-frontend pattern. This backend will be responsible
> for processing the calls made by the client, and will be able to anticipate
> writes on the network.
>
> Read more about the backend-for-frontend pattern in the [atproto][ATPROTO]
> documentation website.
## Advances use-cases

@@ -224,3 +346,2 @@

state,
max_age: 600, // Require re-authentication after 10 minutes
})

@@ -227,0 +348,0 @@ ```

@@ -12,4 +12,2 @@ export * from '@atproto-labs/did-resolver'

export * from './oauth-agent.js'
export * from './oauth-atp-agent.js'
export * from './oauth-authorization-server-metadata-resolver.js'

@@ -23,2 +21,3 @@ export * from './oauth-callback-error.js'

export * from './oauth-server-factory.js'
export * from './oauth-session.js'
export * from './runtime-implementation.js'

@@ -25,0 +24,0 @@ export * from './session-getter.js'

@@ -26,4 +26,2 @@ import {

import { TokenRevokedError } from './errors/token-revoked-error.js'
import { OAuthAgent } from './oauth-agent.js'
import { OAuthAtpAgent } from './oauth-atp-agent.js'
import {

@@ -41,2 +39,3 @@ AuthorizationServerMetadataCache,

import { OAuthServerFactory } from './oauth-server-factory.js'
import { OAuthSession } from './oauth-session.js'
import { RuntimeImplementation } from './runtime-implementation.js'

@@ -267,3 +266,2 @@ import { Runtime } from './runtime.js'

const nonce = await this.runtime.generateNonce()
const pkce = await this.runtime.generatePKCE()

@@ -279,4 +277,3 @@ const dpopKey = await this.runtime.generateKey(

dpopKey,
nonce,
verifier: pkce?.verifier,
verifier: pkce.verifier,
appState: options?.state,

@@ -288,5 +285,4 @@ })

redirect_uri: redirectUri,
code_challenge: pkce?.challenge,
code_challenge_method: pkce?.method,
nonce,
code_challenge: pkce.challenge,
code_challenge_method: pkce.method,
state,

@@ -304,9 +300,4 @@ login_hint: identity

display: options?.display,
id_token_hint: options?.id_token_hint,
max_age: options?.max_age, // this.clientMetadata.default_max_age
prompt: options?.prompt,
scope: options?.scope
?.split(' ')
.filter((s) => metadata.scopes_supported?.includes(s))
.join(' '),
scope: options?.scope || undefined,
ui_locales: options?.ui_locales,

@@ -371,3 +362,3 @@ }

async callback(params: URLSearchParams): Promise<{
agent: OAuthAtpAgent
session: OAuthSession
state: string | null

@@ -445,15 +436,3 @@ }> {

try {
if (tokenSet.id_token) {
await this.runtime.validateIdTokenClaims(
tokenSet.id_token,
stateParam,
stateData.nonce,
codeParam,
tokenSet.access_token,
)
}
const { sub } = tokenSet
await this.sessionGetter.setStored(sub, {
await this.sessionGetter.setStored(tokenSet.sub, {
dpopKey: stateData.dpopKey,

@@ -463,5 +442,5 @@ tokenSet,

const agent = this.createAgent(server, sub)
const session = this.createSession(server, tokenSet.sub)
return { agent, state: stateData.appState ?? null }
return { session, state: stateData.appState ?? null }
} catch (err) {

@@ -480,8 +459,8 @@ await server.revoke(tokenSet.access_token)

/**
* Build an agent from a stored session. This will refresh the token only if
* needed (about to expire) by default.
* Load a stored session. This will refresh the token only if needed (about to
* expire) by default.
*
* @param refresh See {@link SessionGetter.getSession}
*/
async restore(sub: string, refresh?: boolean): Promise<OAuthAtpAgent> {
async restore(sub: string, refresh?: boolean): Promise<OAuthSession> {
const { dpopKey, tokenSet } = await this.sessionGetter.getSession(

@@ -497,3 +476,3 @@ sub,

return this.createAgent(server, sub)
return this.createSession(server, sub)
}

@@ -518,12 +497,5 @@

createAgent(server: OAuthServerAgent, sub: string): OAuthAtpAgent {
const oauthAgent = new OAuthAgent(
server,
sub,
this.sessionGetter,
this.fetch,
)
return new OAuthAtpAgent(oauthAgent)
protected createSession(server: OAuthServerAgent, sub: string): OAuthSession {
return new OAuthSession(server, sub, this.sessionGetter, this.fetch)
}
}
import { Fetch, Json, bindFetch, fetchJsonProcessor } from '@atproto-labs/fetch'
import { SimpleStore } from '@atproto-labs/simple-store'
import { Key, Keyset, SignedJwt } from '@atproto/jwk'
import { Key, Keyset } from '@atproto/jwk'
import {

@@ -29,5 +29,4 @@ CLIENT_ASSERTION_TYPE_JWT_BEARER,

aud: string
scope?: string
scope: string
id_token?: SignedJwt
refresh_token?: string

@@ -132,5 +131,14 @@ access_token: string

const { sub } = tokenResponse
// ATPROTO requires that the "sub" is always present in the token response.
if (!sub) throw new TypeError(`Missing "sub" in token response`)
if (!sub || typeof sub !== 'string') {
throw new TypeError(`Unexpected ${typeof sub} "sub" in token response`)
}
// Using an array to check for the presence of the "atproto" scope (we don't
// want atproto to be a substring of another scope)
const scopes = tokenResponse.scope?.split(' ')
if (!scopes?.includes('atproto')) {
throw new TypeError('Missing "atproto" scope in token response')
}
// @TODO (?) make timeout configurable

@@ -143,3 +151,3 @@ using signal = timeoutSignal(10e3)

if (resolved.metadata.issuer !== this.serverMetadata.issuer) {
if (this.serverMetadata.issuer !== resolved.metadata.issuer) {
// Best case scenario; the user switched PDS. Worst case scenario; a bad

@@ -152,8 +160,8 @@ // actor is trying to impersonate a user. In any case, we must not allow

return {
sub,
aud: resolved.identity.pds.href,
iss: resolved.metadata.issuer,
scope: tokenResponse.scope,
id_token: tokenResponse.id_token,
sub,
scope: tokenResponse.scope!,
refresh_token: tokenResponse.refresh_token,

@@ -160,0 +168,0 @@ access_token: tokenResponse.access_token,

@@ -1,10 +0,6 @@

import { JwtHeader, JwtPayload, Key, unsafeDecodeJwt } from '@atproto/jwk'
import { Key } from '@atproto/jwk'
import { base64url } from 'multiformats/bases/base64'
import { requestLocalLock } from './lock.js'
import {
DigestAlgorithm,
RuntimeImplementation,
RuntimeLock,
} from './runtime-implementation.js'
import { RuntimeImplementation, RuntimeLock } from './runtime-implementation.js'

@@ -41,60 +37,2 @@ export class Runtime {

public async validateIdTokenClaims(
token: string,
state: string,
nonce: string,
code?: string,
accessToken?: string,
): Promise<{
header: JwtHeader
payload: JwtPayload
}> {
// It's fine to use unsafeDecodeJwt here because the token was received from
// the server's token endpoint. The following checks are to ensure that the
// oauth flow was indeed initiated by the client.
const { header, payload } = unsafeDecodeJwt(token)
if (!payload.nonce || payload.nonce !== nonce) {
throw new TypeError('Nonce mismatch')
}
if (payload.c_hash) {
await this.validateHashClaim(payload.c_hash, code, header)
}
if (payload.s_hash) {
await this.validateHashClaim(payload.s_hash, state, header)
}
if (payload.at_hash) {
await this.validateHashClaim(payload.at_hash, accessToken, header)
}
return { header, payload }
}
private async validateHashClaim(
claim: unknown,
source: unknown,
header: { alg: string; crv?: string },
): Promise<void> {
if (typeof claim !== 'string' || !claim) {
throw new TypeError(`string "_hash" claim expected`)
}
if (typeof source !== 'string' || !source) {
throw new TypeError(`string value expected`)
}
const expected = await this.generateHashClaim(source, header)
if (expected !== claim) {
throw new TypeError(`"_hash" does not match`)
}
}
protected async generateHashClaim(
source: string,
header: { alg: string; crv?: string },
) {
const algo = getHashAlgo(header)
const bytes = new TextEncoder().encode(source)
const digest = await this.implementation.digest(bytes, algo)
if (digest.length % 2 !== 0) throw new TypeError('Invalid digest length')
const digestHalf = digest.slice(0, digest.length / 2)
return base64url.baseEncode(digestHalf)
}
public async generatePKCE(byteLength?: number) {

@@ -131,32 +69,2 @@ const verifier = await this.generateVerifier(byteLength)

function getHashAlgo(header: { alg: string; crv?: string }): DigestAlgorithm {
switch (header.alg) {
case 'HS256':
case 'RS256':
case 'PS256':
case 'ES256':
case 'ES256K':
return { name: 'sha256' }
case 'HS384':
case 'RS384':
case 'PS384':
case 'ES384':
return { name: 'sha384' }
case 'HS512':
case 'RS512':
case 'PS512':
case 'ES512':
return { name: 'sha512' }
case 'EdDSA':
switch (header.crv) {
case 'Ed25519':
return { name: 'sha512' }
default:
throw new TypeError('unrecognized or invalid EdDSA curve provided')
}
default:
throw new TypeError('unrecognized or invalid JWS algorithm provided')
}
}
function extractJktComponents(jwk) {

@@ -163,0 +71,0 @@ const get = (field) => {

@@ -6,3 +6,2 @@ import { SimpleStore } from '@atproto-labs/simple-store'

iss: string
nonce: string
dpopKey: Key

@@ -9,0 +8,0 @@ verifier?: string

@@ -19,6 +19,4 @@ import {

// Only for OIDC compatible
// Borrowed from OIDC
ui_locales?: string
id_token_hint?: string
max_age?: number
}

@@ -25,0 +23,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc