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

feat: add form_post response mode support for Apple OAuth #85

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/apple-form-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openauthjs/openauth": patch
---

Add support for form_post response mode in OAuth2 adapter, enabling Apple sign-in compatibility and first-time user info handling
1 change: 1 addition & 0 deletions packages/openauth/src/adapter/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export function AppleAdapter(config: Oauth2WrappedConfig) {
return Oauth2Adapter({
...config,
type: "apple",
responseMode: "form_post",
endpoint: {
authorization: "https://appleid.apple.com/auth/authorize",
token: "https://appleid.apple.com/auth/token",
Expand Down
52 changes: 42 additions & 10 deletions packages/openauth/src/adapter/oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface Oauth2Config {
}
scopes: string[]
query?: Record<string, string>
responseMode?: "query" | "form_post"
}

export type Oauth2WrappedConfig = Omit<Oauth2Config, "endpoint" | "name">
Expand Down Expand Up @@ -48,31 +49,57 @@ export function Oauth2Adapter(config: Oauth2Config) {
authorization.searchParams.set("response_type", "code")
authorization.searchParams.set("state", state)
authorization.searchParams.set("scope", config.scopes.join(" "))
if (config.responseMode) {
authorization.searchParams.set("response_mode", config.responseMode)
}
for (const [key, value] of Object.entries(query)) {
authorization.searchParams.set(key, value)
}
return c.redirect(authorization.toString())
})

routes.get("/callback", async (c) => {
async function handleCallback(c) {
const adapter = (await ctx.get(c, "adapter")) as AdapterState
const code = c.req.query("code")
const state = c.req.query("state")
const error = c.req.query("error")
let code: string | null = null
let state: string | null = null
let error: string | null = null
let errorDescription: string | null = null
let userInfo: Record<string, any> = {}

if (c.req.method === "POST") {
const formData = await c.req.formData()
code = formData.get("code")?.toString() || null
state = formData.get("state")?.toString() || null
error = formData.get("error")?.toString() || null
errorDescription = formData.get("error_description")?.toString() || ""
const user = formData.get("user")?.toString()
if (user) {
try {
userInfo = JSON.parse(user)
} catch (e) {
console.warn("Failed to parse user info:", e)
}
}
} else {
code = c.req.query("code")
state = c.req.query("state")
error = c.req.query("error")
errorDescription = c.req.query("error_description")?.toString() || ""
}

if (error)
throw new OauthError(
error.toString() as any,
c.req.query("error_description")?.toString() || "",
)
throw new OauthError(error.toString() as any, errorDescription)
if (!adapter || !code || (adapter.state && state !== adapter.state))
return c.redirect(getRelativeUrl(c, "./authorize"))

const body = new URLSearchParams({
client_id: config.clientID,
client_secret: config.clientSecret,
code,
grant_type: "authorization_code",
redirect_uri: adapter.redirect,
})

const json: any = await fetch(config.endpoint.token, {
method: "POST",
headers: {
Expand All @@ -81,8 +108,10 @@ export function Oauth2Adapter(config: Oauth2Config) {
},
body: body.toString(),
}).then((r) => r.json())

if ("error" in json)
throw new OauthError(json.error, json.error_description)

return ctx.success(c, {
clientID: config.clientID,
tokenset: {
Expand All @@ -96,11 +125,14 @@ export function Oauth2Adapter(config: Oauth2Config) {
return json.expires_in
},
get raw() {
return json
return { ...json, userInfo }
},
},
})
})
}

routes.get("/callback", handleCallback)
routes.post("/callback", handleCallback)
},
} satisfies Adapter<{ tokenset: Oauth2Token; clientID: string }>
}
Loading