mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 22:59:11 +03:00
Merge pull request #14513 from supabase/hf/add-apple-secret-generator
docs: add apple secret generator
This commit is contained in:
2 files changed
+200
-62
No files matched your search
@@ -0,0 +1,193 @@
|
||||
import { useState } from 'react'
|
||||
import { Input, Button } from 'ui'
|
||||
import Admonition from '~/components/Admonition'
|
||||
|
||||
function base64URL(value: string) {
|
||||
return globalThis.btoa(value).replace(/[=]/g, '').replace(/[+]/g, '-').replace(/[\/]/g, '_')
|
||||
}
|
||||
|
||||
/*
|
||||
Convert a string into an ArrayBuffer
|
||||
from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
||||
*/
|
||||
function stringToArrayBuffer(value: string) {
|
||||
const buf = new ArrayBuffer(value.length)
|
||||
const bufView = new Uint8Array(buf)
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
bufView[i] = value.charCodeAt(i)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
function arrayBufferToString(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint8Array(buf))
|
||||
}
|
||||
|
||||
const generateAppleSecretKey = async (
|
||||
kid: string,
|
||||
iss: string,
|
||||
sub: string,
|
||||
file: File
|
||||
): Promise<{ kid: string; jwt: string; exp: number }> => {
|
||||
if (!kid) {
|
||||
const match = file.name.match(/AuthKey_([^.]+)[.].*$/i)
|
||||
if (match && match[1]) {
|
||||
kid = match[1]
|
||||
}
|
||||
}
|
||||
|
||||
if (!kid) {
|
||||
throw new Error(
|
||||
`No Key ID provided. The file "${file.name}" does not follow the AuthKey_XXXXXXXXXX.p8 pattern. Please provide a Key ID manually.`
|
||||
)
|
||||
}
|
||||
|
||||
const contents = await file.text()
|
||||
|
||||
if (!contents.match(/^\s*-+BEGIN PRIVATE KEY-+[^-]+-+END PRIVATE KEY-+\s*$/i)) {
|
||||
throw new Error(`Chosen file does not appear to be a PEM encoded PKCS8 private key file.`)
|
||||
}
|
||||
|
||||
// remove PEM headers and spaces
|
||||
const pkcs8 = stringToArrayBuffer(
|
||||
globalThis.atob(contents.replace(/-+[^-]+-+/g, '').replace(/\s+/g, ''))
|
||||
)
|
||||
|
||||
const privateKey = await globalThis.crypto.subtle.importKey(
|
||||
'pkcs8',
|
||||
pkcs8,
|
||||
{
|
||||
name: 'ECDSA',
|
||||
namedCurve: 'P-256',
|
||||
},
|
||||
true,
|
||||
['sign']
|
||||
)
|
||||
|
||||
const iat = Math.floor(Date.now() / 1000)
|
||||
const exp = iat + 180 * 24 * 60 * 60
|
||||
|
||||
const jwt = [
|
||||
base64URL(JSON.stringify({ typ: 'JWT', kid, alg: 'ES256' })),
|
||||
base64URL(
|
||||
JSON.stringify({
|
||||
iss,
|
||||
sub,
|
||||
iat,
|
||||
exp,
|
||||
aud: 'https://appleid.apple.com',
|
||||
})
|
||||
),
|
||||
]
|
||||
|
||||
const signature = await globalThis.crypto.subtle.sign(
|
||||
{
|
||||
name: 'ECDSA',
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
privateKey,
|
||||
stringToArrayBuffer(jwt.join('.'))
|
||||
)
|
||||
|
||||
jwt.push(base64URL(arrayBufferToString(signature)))
|
||||
|
||||
return { kid, jwt: jwt.join('.'), exp }
|
||||
}
|
||||
|
||||
const AppleSecretGenerator = () => {
|
||||
const [file, setFile] = useState({ file: null as File | null })
|
||||
const [teamID, setTeamID] = useState('')
|
||||
const [serviceID, setServiceID] = useState('')
|
||||
const [keyID, setKeyID] = useState('')
|
||||
const [secretKey, setSecretKey] = useState('')
|
||||
const [expiresAt, setExpiresAt] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
label="Account ID"
|
||||
labelOptional="required"
|
||||
placeholder="Apple Developer account ID, 10 alphanumeric digits"
|
||||
descriptionText="Found in the upper-right corner of Apple Developer Center."
|
||||
value={teamID}
|
||||
onChange={(e) => setTeamID(e.target.value.trim())}
|
||||
/>
|
||||
<Input
|
||||
label="Service ID"
|
||||
labelOptional="required"
|
||||
placeholder="ID of the service, example: com.example.app.service"
|
||||
descriptionText="Found under Certificates, Identifiers & Profiles in Apple Developer Center."
|
||||
value={serviceID}
|
||||
onChange={(e) => setServiceID(e.target.value.trim())}
|
||||
/>
|
||||
<Input
|
||||
label="Key ID"
|
||||
labelOptional="(optional)"
|
||||
placeholder="Extracted from filename, AuthKey_XXXXXXXXXX.p8"
|
||||
descriptionText="If the file you select does not preserve the original name from Apple Developer Center, please enter the key ID."
|
||||
value={keyID}
|
||||
onChange={(e) => setKeyID(e.target.value.trim())}
|
||||
/>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
setFile({ file: e.target.files[0] })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ height: '1rem' }} />
|
||||
|
||||
<Button
|
||||
size="medium"
|
||||
disabled={
|
||||
!(
|
||||
teamID.length === 10 &&
|
||||
serviceID &&
|
||||
((globalThis && globalThis.showOpenFilePicker) || file.file)
|
||||
)
|
||||
}
|
||||
onClick={async () => {
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const { kid, jwt, exp } = await generateAppleSecretKey(
|
||||
keyID,
|
||||
teamID,
|
||||
serviceID,
|
||||
file.file
|
||||
)
|
||||
setKeyID(kid)
|
||||
setSecretKey(jwt)
|
||||
setExpiresAt(new Date(exp * 1000).toString())
|
||||
setError('')
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
console.error(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Generate Secret Key
|
||||
</Button>
|
||||
|
||||
{error && <Admonition type="danger">{error}</Admonition>}
|
||||
|
||||
{secretKey && (
|
||||
<>
|
||||
<div style={{ height: '1rem' }} />
|
||||
<Input
|
||||
label="Secret Key"
|
||||
value={secretKey}
|
||||
descriptionText={`Valid until: ${expiresAt}. Make sure you generate a new one before then!`}
|
||||
reveal
|
||||
copy
|
||||
size="medium"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppleSecretGenerator
|
||||
@@ -1,4 +1,5 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import AppleSecretGenerator from '~/components/AppleSecretGenerator'
|
||||
|
||||
export const meta = {
|
||||
id: 'auth-apple',
|
||||
@@ -89,69 +90,15 @@ Now you'll need to download a `secret key` file from Apple that will be used to
|
||||
- Save the downloaded file -- this contains your "secret key" that will be used to generate your `client_secret`.
|
||||
- Click `Done` at the top right.
|
||||
|
||||
## Generate a `client_secret`
|
||||
## Generate a client secret
|
||||
|
||||
The `secret key` you downloaded is used to create the `client_secret` string you'll need to authenticate your users.
|
||||
You need to configure a client secret when using Sign in with Apple for Web. This is a specially crafted [JWT signed with a secret key downloaded from Apple's Developer Center](https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens).
|
||||
|
||||
According to the [Apple Docs](https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens) it needs to be a JWT
|
||||
token encrypted using the Elliptic Curve Digital Signature Algorithm (ECDSA) with the P-256 curve and the SHA-256 hash algorithm.
|
||||
<Admonition>
|
||||
Use this tool to generate a new Apple client secret. No keys leave your browser!
|
||||
</Admonition>
|
||||
|
||||
At this time, the easiest way to generate this JWT token is with [Ruby](https://www.ruby-lang.org/en/).
|
||||
If you don't have Ruby installed, you can [Download Ruby Here](https://www.ruby-lang.org/en/downloads).
|
||||
|
||||
- Install Ruby (or check to make sure it's installed on your system).
|
||||
- Install [ruby-jwt](https://github.com/jwt/ruby-jwt).
|
||||
- From the command line, run: `sudo gem install jwt`.
|
||||
|
||||
Create the script below using a text editor: `secret_gen.rb`
|
||||
|
||||
```ruby
|
||||
require "jwt"
|
||||
|
||||
key_file = "Path to the private key"
|
||||
team_id = "Your Team ID"
|
||||
client_id = "The Service ID of the service you created"
|
||||
key_id = "The Key ID of the private key"
|
||||
|
||||
validity_period = 180 # In days. Max 180 (6 months) according to Apple docs.
|
||||
|
||||
private_key = OpenSSL::PKey::EC.new IO.read key_file
|
||||
|
||||
token = JWT.encode(
|
||||
{
|
||||
iss: team_id,
|
||||
iat: Time.now.to_i,
|
||||
exp: Time.now.to_i + 86400 * validity_period,
|
||||
aud: "https://appleid.apple.com",
|
||||
sub: client_id
|
||||
},
|
||||
private_key,
|
||||
"ES256",
|
||||
header_fields=
|
||||
{
|
||||
kid: key_id
|
||||
}
|
||||
)
|
||||
puts token
|
||||
```
|
||||
|
||||
1. Edit the `secret_gen.rb` file:
|
||||
|
||||
- `key_file` = "Path to the private key you downloaded from Apple". It should look like this: `AuthKey_XXXXXXXXXX.p8`.
|
||||
- `team_id` = "Your Team ID". This is found at the Apple Developer website, under Membership details. This is a 10-character alphanumeric string called "Team ID". Alternatively, this can be seen next to your name in the upper right when viewing your Certificates, Identifiers & Profiles.
|
||||
- `client_id` = "The Service ID of the service you created". This is the `Services ID` you created in the above step `Obtain a Services ID`. If you've lost this ID, you can find it in the Apple Developer Site:
|
||||
- Go to `Certificates, Identifiers & Profiles`.
|
||||
- Click `Identifiers` at the left.
|
||||
- At the top right drop-down, select `Services IDs`.
|
||||
- Find your Identifier in the list (i.e. app.com.acme.roadrunner).
|
||||
- `key_id` = "The Key ID of the private key". This can be found in the name of your downloaded secret file (For a file named `AuthKey_XXXXXXXXXX.p8` your key_id is `XXXXXXXXXX`). If you've lost this ID, you can find it in the Apple Developer Site:
|
||||
- Go to `Certificates, Identifiers & Profiles`.
|
||||
- Click `Keys` at the left.
|
||||
- Click on your newly-created key in the list.
|
||||
- Look under `Key ID` to find your key_id.
|
||||
|
||||
2. From the command line, run: `ruby secret_gen.rb > client_secret.txt`.
|
||||
3. Your `client_secret` is now stored in this `client_secret.txt` file.
|
||||
<AppleSecretGenerator />
|
||||
|
||||
## Add your OAuth credentials to Supabase
|
||||
|
||||
@@ -180,8 +127,6 @@ async function signout() {
|
||||
## Resources
|
||||
|
||||
- [Apple Developer Account](https://developer.apple.com).
|
||||
- [Ruby](https://www.ruby-lang.org/en/) Docs.
|
||||
- [ruby-jwt](https://github.com/jwt/ruby-jwt) library.
|
||||
- Thanks to [Janak Amarasena](https://medium.com/@janakda) who did all the heavy lifting in [How to configure Sign In with Apple](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
Reference in new issue
Block a user