Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚧 OAuth2 - Authorization Server #1958

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
140 commits
Select commit Hold shift + click to select a range
cc29615
fix(loggerMiddleware): avoid 500 when invalid jwt used as bearer & ad…
matthieusieben Mar 4, 2024
604dce4
refactor(pds): add refreshExpired auth verifier
matthieusieben Feb 28, 2024
6386333
refactor(pds:auth-verifier): add jwtVerify instance method
matthieusieben Feb 28, 2024
4af72bc
eat(oauth-provider): initial implementation
matthieusieben Feb 14, 2024
813d614
add oauth provider capability to PDS
matthieusieben Feb 22, 2024
9368180
feat(pds): split tracer code in own file
matthieusieben Mar 5, 2024
8db0d74
chore(dev): add dev script
matthieusieben Mar 5, 2024
fb6fed6
dps: update example .env file
matthieusieben Mar 5, 2024
9c56ed4
build: disable allowImportingTsExtensions
matthieusieben Mar 7, 2024
fb84144
feat(oauth-provider): render login errors using HTML
matthieusieben Mar 7, 2024
75b134f
feat(pds): prepare account enrichment
matthieusieben Mar 7, 2024
ff749ea
fix(pds): better type ReqCtx
matthieusieben Mar 7, 2024
e15e675
small improvements to the fetch-node package
matthieusieben Mar 8, 2024
500eaa2
feat(oauth-provider): allow customizing branding of the login page
matthieusieben Mar 8, 2024
084e629
feat(pds): use Bluesky blue as primary color
matthieusieben Mar 8, 2024
55bd204
feat(oauth-provider): improve customizability
matthieusieben Mar 9, 2024
77d0ad3
feat(pds): change login column color
matthieusieben Mar 9, 2024
583cbab
feat: prepare for branding page
matthieusieben Mar 10, 2024
aa55960
feat: add welcome page
matthieusieben Mar 12, 2024
576722e
fix: minor ux improvement
matthieusieben Mar 12, 2024
6cbcc58
feat(pds): load branding from env
matthieusieben Mar 13, 2024
70b1256
chore: use "default" instead of "require" export condition
matthieusieben Mar 13, 2024
480a3d3
chore: update rollupjs
matthieusieben Mar 13, 2024
a7423b5
feat: improve error management
matthieusieben Mar 13, 2024
811d28e
chore(deps): remove unused lru-cache
matthieusieben Mar 14, 2024
02a5688
chore(dev): add blobs to ignored files
matthieusieben Mar 14, 2024
c162268
fix(oauth-provider): various UI improvements
matthieusieben Mar 14, 2024
d12a897
feat(dps): add profile info to oauth account selector
matthieusieben Mar 14, 2024
490dc87
fix(oauth-provider): do not throw when essential claims are not avail…
matthieusieben Mar 15, 2024
8aa42d8
fix(oauth-provider): use InvalidRequestError instead AccessDeniedErro…
matthieusieben Mar 15, 2024
7af0c68
fix(oauth-provider): throw invalid_client_metadata if require_auth_ti…
matthieusieben Mar 15, 2024
eb0f86d
docs(oauth-provider): clarify comment
matthieusieben Mar 15, 2024
5297899
fix(oauth-provider): throw UnauthorizedError (401) error if login fails
matthieusieben Mar 15, 2024
d6d98b8
feat(oauth-provider): redirect the user to the client whenever possib…
matthieusieben Mar 15, 2024
433867e
feat(html): allow nullish & false values in template literal variables
matthieusieben Mar 15, 2024
a0c9710
refactor: split html page generation logic
matthieusieben Mar 15, 2024
27f0400
fix(oauth-provider): remove support for password grant
matthieusieben Mar 15, 2024
9231564
refactor(oauth-provider)!: authentication step returns entire account…
matthieusieben Mar 15, 2024
f3cb3ff
style: minor changes to signIn method
matthieusieben Mar 15, 2024
544f22e
refactor: rename "branding" into "customization"
matthieusieben Mar 15, 2024
b085fec
refactor(oauth-provider): split accept view in own file
matthieusieben Mar 15, 2024
204376e
fix(oauth-provider): improve welcome view UI
matthieusieben Mar 15, 2024
7552930
feat(oauth-provider): add customization links as <link> html elements
matthieusieben Mar 18, 2024
a479226
fix(oauth-provider): add csp headers to post redirect page
matthieusieben Mar 18, 2024
abe073d
perf(html): avoid generating fragments when concatenating Html
matthieusieben Mar 18, 2024
34107cb
style(oauth-provider): improve separation of concerns
matthieusieben Mar 18, 2024
3047920
refactor: group code
matthieusieben Mar 18, 2024
2abda74
feat(oauth-provider): allow customization of sign-in fields
matthieusieben Mar 18, 2024
47f6207
wip: sign-up
matthieusieben Mar 18, 2024
8bae593
feat: allow dpopSecret to be left empty
matthieusieben Mar 19, 2024
a622709
feat(fetch): add timeout in safe fetch mode
matthieusieben Mar 20, 2024
0ce839c
refactor: factorize client-metadata in own lib
matthieusieben Mar 21, 2024
d24bb08
feat(caching): add generic caching utils
matthieusieben Mar 21, 2024
60c0861
feat(oauth-provider-client-uri): cache http responses
matthieusieben Mar 21, 2024
0f6a30c
feat(oauth-server-metadata): add OAuth server metadata validation uti…
matthieusieben Mar 21, 2024
3f54b6d
refactor(oauth-provider): use shared oauth-server-metadata lib
matthieusieben Mar 21, 2024
23c6e38
feat(oauth-server-metadata-resolver): utility class that allows fetch…
matthieusieben Mar 21, 2024
b2bde5d
feat(jwk): allow searching keys using multiple kids
matthieusieben Mar 22, 2024
b5fdcd6
fix(oauth-provider): read pushed_authorization_request_endpoint_auth_…
matthieusieben Mar 22, 2024
e53c45b
feat(fetch)!: expose response object in json processor result
matthieusieben Mar 23, 2024
6346096
feat(did): create isomorphic DID utility lib
matthieusieben Mar 21, 2024
bf2c755
feat(handle-resolver): isomorphic and node specific handle resolution…
matthieusieben Mar 21, 2024
d5f1230
docs(oauth-provider): add toto comment
matthieusieben Mar 22, 2024
a0e82a6
chore(ts): allow fall through in switch
matthieusieben Mar 23, 2024
91d0b1e
feat(b64): add base64-url encoding util
matthieusieben Mar 23, 2024
05c256c
refactor(jwk)!: extract JOSE specific code in own package
matthieusieben Mar 23, 2024
5b6fcdd
docs(oauth-provider): order response_types_supported by category
matthieusieben Mar 23, 2024
20474b5
fix(fetch): cancel timeout on end of response
matthieusieben Mar 25, 2024
3335da0
feat(fetch-dpop): add dpop fetch util
matthieusieben Mar 25, 2024
ecffdc3
feat(identity-resolver): add identity resolver package
matthieusieben Mar 28, 2024
9dda100
fix(oauth-provider): limit dpop validity time
matthieusieben Mar 28, 2024
0c9a980
feat(disposable-polyfill): init
matthieusieben Apr 3, 2024
455c026
feat(jwk-webcrypto): init
matthieusieben Apr 3, 2024
087fa2f
feat(indexed-db): init
matthieusieben Apr 3, 2024
d3862ed
style(oauth-provider): lint
matthieusieben Apr 3, 2024
49afd67
feat(oauth-client): oauth client
matthieusieben Mar 28, 2024
08814d1
docs(oauth-provider): improve jsdoc
matthieusieben Apr 8, 2024
885fc51
feat(oauth-provider): better define "signIn" API argument
matthieusieben Apr 8, 2024
2ade64a
refactor(pds): implement BasicProfileGetter using CachedGetter
matthieusieben Apr 8, 2024
9e263f7
feat(oauth-client): support working with dev-env
matthieusieben Apr 8, 2024
fc612a8
feat(dev-env): add oauth config
matthieusieben Apr 8, 2024
fe7e54f
feat(dev): run dev-env as part of "pnpm dev"
matthieusieben Apr 8, 2024
4f9d70e
fix(oauth-provider): do not require "implicit" grant_type for respons…
matthieusieben Apr 8, 2024
7038535
feat(oauth-provider): expose allowed redirect_uris in "invalid redire…
matthieusieben Apr 8, 2024
373e154
docs(oauth-provider): improve comments in redirect_uri matching algor…
matthieusieben Apr 8, 2024
08810b8
fix(pds:oauth): add missing "openid" scope for localhost clients
matthieusieben Apr 8, 2024
eb1bfa8
fix(oauth-provider-client-uri): improve validation of client id
matthieusieben Apr 8, 2024
892a3ef
fix(oauth-client): improve validation of client metadata
matthieusieben Apr 8, 2024
d2e29a7
fix(oauth-client-browser): allow customizing the atprotoLexiconUrl
matthieusieben Apr 8, 2024
6311aa7
feat(oauth-client-browser-example): use loopback client metadata
matthieusieben Apr 8, 2024
9750371
fix(deps): update pnpm-lock
matthieusieben Apr 9, 2024
14fa216
fix(pds:oauth): typings
matthieusieben Apr 9, 2024
a3a0c2b
chore(build): use tsc --build mode
matthieusieben Apr 10, 2024
a2b5863
fix(oauth-provider): better align errors with spec
matthieusieben Apr 10, 2024
ec5201e
fix(fetch): make sure the body is consumed in case of http error
matthieusieben Apr 10, 2024
f9bdc6c
fix(dpop-fetch): properly parse "use_dpop_nonce" error in response
matthieusieben Apr 10, 2024
85ad5b0
fix(handle-resolver): any invalid payload should be considered as "null"
matthieusieben Apr 10, 2024
af71160
feat(http-util): expose context utilities
matthieusieben Apr 10, 2024
1c3633e
fix(pds): type cast json.parse result
matthieusieben Apr 10, 2024
9afaa6e
fix(pds): expose "use_dpop_nonce" error in WWW-Authenticate header
matthieusieben Apr 10, 2024
9594589
feat(oauth-server-metadata): expose issuer schema
matthieusieben Apr 11, 2024
1574b4d
fix(oauth-server-metadata-resolver): use private ts fields instead of…
matthieusieben Apr 11, 2024
f902845
refactor(oauth-server-metadata-resolver)!: use named export instead o…
matthieusieben Apr 11, 2024
22a4830
refactor(oauth-client)!: use ISO Dates to convey session expiry
matthieusieben Apr 11, 2024
3c7c5fb
fix(dev-env): move oauth pds config to TestPds
matthieusieben Apr 11, 2024
b319f04
feat(oauth-provider): add onAccountAddAuthorizedClient hook
matthieusieben Apr 11, 2024
d44abef
fix(oauth-provider): add missing scopes_supported
matthieusieben Apr 11, 2024
549978e
fix(oauth-provider): avoid throwing "resource owner interaction" rela…
matthieusieben Apr 11, 2024
dfcfb9b
docs(oauth-provider): better document AuthorizationRequestHook
matthieusieben Apr 11, 2024
d3ff07d
feat(oauth-provider): export Client class
matthieusieben Apr 11, 2024
572a41e
fix(pds): prevent loopback clients from obtaining refresh tokens
matthieusieben Apr 11, 2024
0ac74eb
fix(pds): prevent un-authenticated clients from obtaining refresh tokens
matthieusieben Apr 11, 2024
60295c8
fix(oauth-client): use "invalid_grant" oauth error to detect that ref…
matthieusieben Apr 11, 2024
9853a7c
fix(oauth-server-metadata): ensure response_types_supported includes …
matthieusieben Apr 11, 2024
ecd7be3
feat(oauth-client-metadata): export response type and grant type sche…
matthieusieben Apr 11, 2024
5b6935b
fix(oauth-client): use OAuthResponseType from oauth-client-metadata
matthieusieben Apr 11, 2024
8f8f8c6
fix(oauth-client): negotiate response_type request param with server
matthieusieben Apr 11, 2024
cf5f82d
fix(oauth-client): remove unused import
matthieusieben Apr 11, 2024
20d4255
fix(oauth-client-browser): remove "responseType" from factory config
matthieusieben Apr 11, 2024
d157fdb
feat(identity-resolver): expose typed used in options
matthieusieben Apr 12, 2024
fc35a35
fix(oauth-client-browser): typo in options type
matthieusieben Apr 12, 2024
a1bd979
feat(oauth-client-react-native): shell
matthieusieben Apr 12, 2024
812b60f
fix(fetch): avoid relying on .blob()
matthieusieben Apr 12, 2024
56aa243
docs(oauth-client-react-native): add implementation details
matthieusieben Apr 12, 2024
718b387
fix(jwk-webcrypto): remove unused code
matthieusieben Apr 12, 2024
5241de0
fix(jwk-webcrypto): prefer jwk algorithms that yield smaller signatures
matthieusieben Apr 12, 2024
4cc3257
fix(oauth-client): prefer jwk algorithms that yield smaller signatures
matthieusieben Apr 12, 2024
316b5f6
feat(jwk): export jwkValidator that validates "use" and "key_ops" claims
matthieusieben Apr 12, 2024
d8748be
feat(oauth-client-react-native): validate native jwk
matthieusieben Apr 12, 2024
065e0b6
fix(jose-key): remove un-necessary code
matthieusieben Apr 12, 2024
8d14b78
fix(fetch): do not attempts to cancel the body if it was used
matthieusieben Apr 12, 2024
7e9b72e
fix(oauth-provider): do not expose account info in browser
matthieusieben Apr 12, 2024
e316117
feat(jwk): simplify VerifyOptions interface
matthieusieben Apr 12, 2024
f55c1bd
feat(oauth-client-react-native): improve typings
matthieusieben Apr 12, 2024
3eefb79
docs(oauth-client-react-native): add jsdoc
matthieusieben Apr 12, 2024
fdfe438
refactor(oauth-provider): merge session and device concepts
matthieusieben Apr 15, 2024
9c794e0
fix(pds): adapt to latest oauth-provider refactor
matthieusieben Apr 15, 2024
17fc0fc
fixup! refactor(oauth-provider): merge session and device concepts
matthieusieben Apr 15, 2024
b01e289
fixup! refactor(oauth-provider): merge session and device concepts
matthieusieben Apr 15, 2024
6aa38d7
feat(oauth-client-browser): simplify login ux & allow popup mode
matthieusieben Apr 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"pino-pretty": "^9.1.0",
"prettier": "^3.2.5",
"prettier-config-standard": "^7.0.0",
"react-native": "^0.73.6",
"typescript": "^5.4.4"
},
"workspaces": {
Expand Down
28 changes: 28 additions & 0 deletions packages/b64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@atproto/b64",
"version": "0.0.1",
"license": "MIT",
"description": "A library for encoding/decoding in base64-url",
"keywords": [
"atproto",
"base64",
"base64-url"
],
"homepage": "https://atproto.com",
"repository": {
"type": "git",
"url": "https://github.com/bluesky-social/atproto",
"directory": "packages/b64"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"base64-js": "^1.5.1"
},
"devDependencies": {
"typescript": "^5.3.3"
},
"scripts": {
"build": "tsc --build tsconfig.json"
}
}
34 changes: 34 additions & 0 deletions packages/b64/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { fromByteArray, toByteArray } from 'base64-js'

// Old Node implementations do not support "base64url"
const Buffer = ((Buffer) => {
if (typeof Buffer === 'function') {
try {
Buffer.from('', 'base64url')
return Buffer
} catch {
return undefined
}
}
return undefined
})(globalThis.Buffer)

export const b64uDecode: (b64u: string) => Uint8Array = Buffer
? (b64u) => Buffer.from(b64u, 'base64url')
: (b64u) => {
// toByteArray requires padding but not to replace '-' and '_'
const pad = b64u.length % 4
const b64 = b64u.padEnd(b64u.length + (pad > 0 ? 4 - pad : 0), '=')
return toByteArray(b64)
}

export const b64uEncode = Buffer
? (bytes: Uint8Array) => {
const buffer = bytes instanceof Buffer ? bytes : Buffer.from(bytes)
return buffer.toString('base64url')
}
: (bytes: Uint8Array): string =>
fromByteArray(bytes)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '')
8 changes: 8 additions & 0 deletions packages/b64/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig/isomorphic.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
65 changes: 45 additions & 20 deletions packages/bsky/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,57 @@ export const loggerMiddleware = pinoHttp({
},
req: (req) => {
const serialized = pino.stdSerializers.req(req)
const authHeader = serialized.headers.authorization || ''
let auth: string | undefined = undefined
if (authHeader.startsWith('Bearer ')) {
const token = authHeader.slice('Bearer '.length)
const { iss } = jose.decodeJwt(token)
if (iss) {
auth = 'Bearer ' + iss
} else {
auth = 'Bearer Invalid'
}
}
if (authHeader.startsWith('Basic ')) {
const parsed = parseBasicAuth(authHeader)
if (!parsed) {
auth = 'Basic Invalid'
} else {
auth = 'Basic ' + parsed.username
}
}
const authHeader = serialized.headers.authorization

if (authHeader == null) return serialized

return {
...serialized,
headers: {
...serialized.headers,
authorization: auth,
authorization: obfuscateAuthHeader(authHeader),
},
}
},
},
})

function obfuscateAuthHeader(authHeader: string): string {
const [type, token] = authHeader.split(' ', 2)
switch (type) {
case 'Basic':
return `${type} ${obfuscateBasic(authHeader!)}`
case 'Bearer':
case 'DPoP':
return `${type} ${obfuscateBearer(token)}`
default:
return `Invalid`
}
}

function obfuscateBasic(authHeader: string): string {
const parsed = parseBasicAuth(authHeader)
if (parsed) return parsed.username
return 'Invalid'
}

function obfuscateBearer(token?: string): string {
if (token) {
if (token.includes('.')) {
try {
const { sub } = jose.decodeJwt(token)
if (sub) return sub
} catch {
// Not a JWT
}
}

if (token.length > 10) {
// Log no more than half the token, up to 10 characters. tokens should be
// long enough to be secure even when half of them are exposed.
return `${token.slice(0, Math.min(10, token.length / 2))}...`
}
}

return 'Invalid'
}
24 changes: 24 additions & 0 deletions packages/caching/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@atproto/caching",
"version": "0.0.1",
"license": "MIT",
"description": "Small caching utilities",
"keywords": [
"caching",
"isomorphic"
],
"homepage": "https://atproto.com",
"repository": {
"type": "git",
"url": "https://github.com/bluesky-social/atproto",
"directory": "packages/caching"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc --build tsconfig.build.json"
},
"dependencies": {
"lru-cache": "^10.2.0"
}
}
141 changes: 141 additions & 0 deletions packages/caching/src/cached-getter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Awaitable, GenericStore, Key, Value } from './generic-store.js'
import { MemoryStore } from './memory-store.js'

export type GetOptions = {
signal?: AbortSignal
noCache?: boolean
allowStale?: boolean
}

export type Getter<K, V> = (
key: K,
options?: GetOptions,
storedValue?: V,
) => Awaitable<V>

export type PendingItem<V> = {
promise: Promise<V>
allowCached: boolean
signal?: AbortSignal
}

export type CachedGetterOptions<K, V> = {
isStale?: (key: K, value: V) => boolean | PromiseLike<boolean>
onStoreError?: (err: unknown, key: K, value: V) => void | PromiseLike<void>
deleteOnError?: (
err: unknown,
key: K,
value: V,
) => boolean | PromiseLike<boolean>
}

/**
* Wrapper utility that uses a cache to speed up the retrieval of values from a
* getter function.
*/
export class CachedGetter<K extends Key = string, V extends Value = Value> {
private pending = new Map<K, PendingItem<V>>()

constructor(
readonly getter: Getter<K, V>,
readonly store: GenericStore<K, V> = new MemoryStore<K, V>({
max: 1000,
ttl: 600e3,
}),
readonly options?: CachedGetterOptions<K, V>,
) {}

async get(key: K, options?: GetOptions): Promise<V> {
const allowCached = options?.noCache !== true
const allowStale =
this.options?.isStale == null ? true : options?.allowStale ?? false

const checkCached = async (value: V) =>
allowCached &&
(allowStale || (await this.options?.isStale?.(key, value)) !== true)

// As long as concurrent requests are made for the same key, only one
// request will be made to the cache & getter function. This works because
// there is no async operation between the while() loop and the
// pending.set() call. Because of the "single threaded" nature of
// JavaScript, the pending item will be set before the next iteration of the
// while loop.
let pending: undefined | PendingItem<V>
while ((pending = this.pending.get(key))) {
options?.signal?.throwIfAborted()

try {
const value = await pending.promise

const isFresh = !pending.allowCached
if (isFresh || (await checkCached(value))) {
return value
}
} catch {
// Ignore errors from pending promises.
}
}

options?.signal?.throwIfAborted()

try {
const promise = Promise.resolve().then(async () => {
const storedValue = await this.getStored(key, options)
if (storedValue !== undefined) {
if (await checkCached(storedValue)) {
return storedValue
}
}

return Promise.resolve()
.then(async () => this.getter(key, options, storedValue))
.catch(async (err) => {
if (storedValue !== undefined && this.options?.deleteOnError) {
if (await this.options.deleteOnError(err, key, storedValue)) {
await this.delStored(key)
}
}
throw err
})
.then(async (value) => {
await this.setStored(key, value)
return value
})
})

this.pending.set(key, {
promise,
signal: options?.signal,
allowCached,
})

return await promise
} finally {
this.pending.delete(key)
}
}

bind(key: K): (options?: GetOptions) => Promise<V> {
return async (options) => this.get(key, options)
}

async getStored(key: K, options?: GetOptions): Promise<V | undefined> {
try {
return await this.store.get(key, options)
} catch (err) {
return undefined
}
}

async setStored(key: K, value: V): Promise<void> {
try {
await this.store.set(key, value)
} catch (err) {
await this.options?.onStoreError?.(err, key, value)
}
}

async delStored(key: K): Promise<void> {
await this.store.del(key)
}
}
18 changes: 18 additions & 0 deletions packages/caching/src/generic-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type Awaitable<V> = V | PromiseLike<V>

export type Key = string | number
export type Value = NonNullable<unknown> | null

export type StoreGetOptions = {
signal?: AbortSignal
}

export interface GenericStore<K extends Key, V extends Value = Value> {
/**
* @return undefined if the key is not in the cache.
*/
get: (key: K, options?: StoreGetOptions) => Awaitable<undefined | V>
set: (key: K, value: V) => Awaitable<void | this>
del: (key: K) => Awaitable<void | this>
clear?: () => Awaitable<void | this>
}
3 changes: 3 additions & 0 deletions packages/caching/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './cached-getter.js'
export * from './generic-store.js'
export * from './memory-store.js'
Loading
Loading