ALPHA SOFTWARE, USE AT YOUR OWN RISK! LOTS MORE TO COME IN THE NEXT WEEK!
Tired of getting logged out of your AT Protocol applications every 48 hours? Introducing OATProxy! OATProxy acts as a transparent passthrough XRPC proxy between your front-end application, upgrading your users from frequently-expiring "public" OAuth sessions to robust, hearty "confidential" OAuth sessions.
| Session Type | Inactivity Timeout | Max Session Length |
|---|---|---|
| Public | 2 days | 7 days |
| Confidential | 1 month | 1 year |
OATProxy exists as both a Go library for embedding in applications and as a standalone microservice.
You'll need:
- A public HTTPS address that forwards to this server. (Built-in TLS is coming!)
- A
client-metadata.jsonfile. You can customize theclient-metadata.example.jsonfile in this repo. - An ATProto app that already works with "public" OAuth.
go install github.com/streamplace/oatproxy/cmd/oatproxy@latest
oatproxy --host=example.com --client-metadata=client-metadata.json
The server will then be available to handle requests on port 8080.
Optionally, OATProxy can operate as a reverse proxy for another application
server behind it with the --upstream-host parameter. If you operate in this
mode, OATProxy will handle all requests for /oauth, /xrpc, and the OAuth
documents in /.well-known. All other requests will be proxied upstream.
(This also applies to @streamplace/oauth-client-react-native.)
For this to work, you're going to have to tell some lies. Specifically, you're
going to need to tell @atproto/oauth-client-browser that, no matter who the
user is, their PDS URL is OATProxy's URL. This can be accomplished by overriding
the fetch handler passed to the client:
import{BrowserOAuthClient,OAuthClient}from"@atproto/oauth-client-browser";constfetchWithLies=async(oatProxyUrl: string,input: RequestInfo|URL,init?: RequestInit)=>{// Normalize input to a Request objectletrequest: Request;if(typeofinput==="string"||inputinstanceofURL){request=newRequest(input,init);}else{request=input;}if(request.url.includes("plc.directory")||// did:plcrequest.url.endsWith("did.json")// did:web){constres=awaitfetch(request,init);if(!res.ok){returnres;}constdata=awaitres.json();constservice=data.service.find((s: any)=>s.id==="#atproto_pds");if(!service){returnres;}service.serviceEndpoint=oatProxyUrl;returnnewResponse(JSON.stringify(data),{status: res.status,headers: res.headers,});}returnfetch(request,init);};exportdefaultasyncfunctioncreateOAuthClient(oatProxyUrl: string): Promise<OAuthClient>{returnawaitBrowserOAuthClient.load({clientId: `${oatProxyUrl}/oauth/downstream/client-metadata.json`,handleResolver: oatProxyUrl,responseMode: "query",// Lie to the oauth client and use our upstream server insteadfetch: (input,init)=>fetchWithLies(oatProxyUrl,input,init),});}If you want to be able to use PDS URLs (this includes the oauth signup flow), add this code as well:
importtype{OAuthAuthorizationServerMetadata}from"@atproto/oauth-client";import{OAuthResolver,ResolveOAuthOptions,}from"@atproto/oauth-client/dist/oauth-resolver";exporttypeStreamplaceOAuthClient=Omit<ReactNativeOAuthClient,"keyset"|"serverFactory"|"jwks">;// A custom OAuth resolver that always fetches metadata from our backend// but remembers the resource server URL for use in login_hintclassStreamplaceOAuthResolverextendsOAuthResolver{privatecurrentResourceServer: string|null=null;constructor(privatestreamplaceUrl: string,
...args: ConstructorParameters<typeofOAuthResolver>){super(...args);}asyncresolveFromService(input: string,options?: ResolveOAuthOptions,): Promise<{metadata: OAuthAuthorizationServerMetadata;}>{// Input is the resource server URL (e.g., https://selfhosted.social)// Store it for use in login_hintthis.currentResourceServer=input;// Always fetch metadata from our backend// The issuer will be our backend, not the resource serverconstmetadata=awaitthis.getResourceServerMetadata(this.streamplaceUrl,options,);return{ metadata };}getCurrentResourceServer(): string|null{returnthis.currentResourceServer;}}// In your createOAuthClient function, before creating the client:letcustomResolver: StreamplaceOAuthResolver|null=null;// In your fetch handler, add this before the existing logic:// Add login_hint parameter to PAR requestsif(customResolver&&request.url.includes("/oauth/par")&&request.method==="POST"){constresourceServer=customResolver.getCurrentResourceServer();if(resourceServer){constclonedRequest=request.clone();constbody=awaitclonedRequest.text();constparams=newURLSearchParams(body);params.set("login_hint",resourceServer);request=newRequest(request.url,{method: request.method,headers: request.headers,body: params.toString(),});}}// After creating the client, replace the default OAuth resolver:customResolver=newStreamplaceOAuthResolver(oatProxyUrl,client.oauthResolver.identityResolver,client.oauthResolver.protectedResourceMetadataResolver,client.oauthResolver.authorizationServerMetadataResolver,);// @ts-ignore override readonly propertyclient.oauthResolver=customResolver;These can be useful for debugging purposes:
| URL | Description |
|---|---|
/oauth/downstream/client-metadata.json | "Public" client metadata document presented to the "downstream" browser client |
/oauth/upstream/client-metadata.json | "Confidential" client metadata presented to the "upstream" PDS |
make
- Many tests
- Built-in TLS support
- Simple local example
- Docker image
- Postgres support
- Document usage as a library
- Document usage on a worker of some kind
- Document usage with atcute
- Ship
@streamplace/atproto-oauth-client-isomorphicthat tells lies automatically
This library brought to you by Streamplace. "Upstream" Go ATProto OAuth client forked from haileyok.