-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'kindfi-org:develop' into fix/open-redirect-validation
- Loading branch information
Showing
13 changed files
with
1,122 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
# Usage | ||
|
||
## API Endpoints | ||
|
||
- **Generate Registration Options:** | ||
|
||
`POST /api/passkey/generate-registration-options` | ||
|
||
- Request Body: `{ "identifier": "[email protected]" }` | ||
- Success Response (200): | ||
```json | ||
{ | ||
// WebAuthn registration options | ||
} | ||
``` | ||
- Error Response (500): | ||
```json | ||
{ | ||
"error": "No matching RP ID found for the given origin" | ||
} | ||
``` | ||
|
||
- **Verify Registration:** | ||
|
||
`POST /api/passkey/verify-registration` | ||
|
||
- Request Body: `{ "identifier": "[email protected]", "registrationResponse": {...} }` | ||
- Success Response (200): | ||
```json | ||
{ | ||
// Verification result | ||
} | ||
``` | ||
- Error Response (500): | ||
```json | ||
{ | ||
"error": "Challenge not found" | ||
} | ||
``` | ||
|
||
- **Generate Authentication Options:** | ||
|
||
`POST /api/passkey/generate-authentication-options` | ||
|
||
- Request Body: `{ "identifier": "[email protected]" }` | ||
- Success Response (200): | ||
```json | ||
{ | ||
// WebAuthn authentication options | ||
} | ||
``` | ||
- Error Response (500): | ||
```json | ||
{ | ||
"error": "Authenticator not registered" | ||
} | ||
``` | ||
|
||
- **Verify Authentication:** | ||
|
||
`POST /api/passkey/verify-authentication` | ||
|
||
- Request Body: `{ "identifier": "[email protected]", "authenticationResponse": {...} }` | ||
- Success Response (200): | ||
```json | ||
{ | ||
// Verification result | ||
} | ||
``` | ||
- Error Response (500): | ||
```json | ||
{ | ||
"error": "Challenge not found" | ||
} | ||
``` | ||
|
||
### Common Error Messages | ||
|
||
- "No matching RP ID found for the given origin" | ||
- "Challenge not found" | ||
- "Authenticator not registered" | ||
- "An unexpected error occurred" | ||
|
||
## Flow | ||
|
||
### Registration Flow (Sequence Diagram) | ||
|
||
```mermaid | ||
sequenceDiagram | ||
participant C as Client | ||
participant R as Registration API | ||
participant P as Passkey Service | ||
participant D as Redis | ||
|
||
C->>R: POST /generate-registration-options | ||
R->>P: Parse request & extract `identifier` and `origin` | ||
P->>D: Save challenge for user | ||
D-->>P: Challenge saved with TTL | ||
P-->>R: Return registration options (or error) | ||
R-->>C: JSON response with registration options | ||
``` | ||
|
||
Summary: | ||
|
||
1. **Request Registration Options**: The user device initiates the registration process by requesting options from the API. | ||
2. **Return WebAuthn Registration Options**: The API responds with the necessary WebAuthn registration options. | ||
3. **Create Registration Response**: The user device uses these options to create a registration response. | ||
4. **Verify Registration**: The API verifies the registration response and returns the result to the user device. | ||
|
||
### Authentication Flow (Sequence Diagram) | ||
|
||
```mermaid | ||
sequenceDiagram | ||
participant C as Client | ||
participant V as Verification API | ||
participant P as Passkey Service | ||
participant D as Redis | ||
C->>V: POST /verify-registration or /verify-authentication | ||
V->>P: Parse request & construct input | ||
P->>D: Retrieve stored challenge | ||
D-->>P: Challenge data (or not found) | ||
P-->>V: Return verification result (or error) | ||
V-->>C: JSON response with verification outcome | ||
``` | ||
|
||
Summary: | ||
|
||
1. **Request Authentication Options**: The user device requests authentication options from the API. | ||
2. **Return WebAuthn Authentication Options**: The API provides the necessary WebAuthn authentication options. | ||
3. **Create Authentication Response**: The user device creates an authentication response using the provided options. | ||
4. **Verify Authentication**: The API verifies the authentication response and returns the result to the user device. |
24 changes: 24 additions & 0 deletions
24
apps/web/app/api/passkey/generate-authentication-options/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { type NextRequest, NextResponse } from 'next/server' | ||
import { InAppError } from '~/lib/passkey/errors' | ||
import { getAuthenticationOptions } from '~/lib/passkey/passkey' | ||
|
||
export async function POST(req: NextRequest) { | ||
try { | ||
const body = await req.json() | ||
const input = { | ||
identifier: body.identifier, | ||
origin: req.headers.get('origin') || body.origin || '', | ||
challenge: body.challenge, | ||
} | ||
const options = await getAuthenticationOptions(input) | ||
return NextResponse.json(options) | ||
} catch (error) { | ||
if (error instanceof InAppError) { | ||
return NextResponse.json({ error: error.message }, { status: 500 }) | ||
} | ||
return NextResponse.json( | ||
{ error: 'Failed to generate authentication options' }, | ||
{ status: 500 }, | ||
) | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
apps/web/app/api/passkey/generate-registration-options/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { type NextRequest, NextResponse } from 'next/server' | ||
import { InAppError } from '~/lib/passkey/errors' | ||
import { getRegistrationOptions } from '~/lib/passkey/passkey' | ||
|
||
export async function POST(req: NextRequest) { | ||
try { | ||
const body = await req.json() | ||
const input = { | ||
identifier: body.identifier, | ||
origin: req.headers.get('origin') || body.origin || '', | ||
} | ||
const options = await getRegistrationOptions(input) | ||
return NextResponse.json(options) | ||
} catch (error) { | ||
if (error instanceof InAppError) { | ||
return NextResponse.json({ error: error.message }, { status: 500 }) | ||
} | ||
return NextResponse.json( | ||
{ error: 'Failed to generate registration options' }, | ||
{ status: 500 }, | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { type NextRequest, NextResponse } from 'next/server' | ||
import { InAppError } from '~/lib/passkey/errors' | ||
import { verifyAuthentication } from '~/lib/passkey/passkey' | ||
|
||
export async function POST(req: NextRequest) { | ||
try { | ||
const body = await req.json() | ||
const input = { | ||
identifier: body.identifier, | ||
origin: req.headers.get('origin') || body.origin || '', | ||
authenticationResponse: body.authenticationResponse, | ||
} | ||
const options = await verifyAuthentication(input) | ||
return NextResponse.json(options) | ||
} catch (error) { | ||
console.error('Error verifying authentication', error) | ||
if (error instanceof InAppError) { | ||
return NextResponse.json({ error: error.message }, { status: 500 }) | ||
} | ||
return NextResponse.json( | ||
{ error: 'Failed to verify authentication' }, | ||
{ status: 500 }, | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { type NextRequest, NextResponse } from 'next/server' | ||
import { InAppError } from '~/lib/passkey/errors' | ||
import { verifyRegistration } from '~/lib/passkey/passkey' | ||
|
||
export async function POST(req: NextRequest) { | ||
try { | ||
const body = await req.json() | ||
const input = { | ||
identifier: body.identifier, | ||
origin: req.headers.get('origin') || body.origin || '', | ||
registrationResponse: body.registrationResponse, | ||
} | ||
const options = await verifyRegistration(input) | ||
return NextResponse.json(options) | ||
} catch (error) { | ||
if (error instanceof InAppError) { | ||
return NextResponse.json({ error: error.message }, { status: 500 }) | ||
} | ||
console.error('Error verifying registration', error) | ||
return NextResponse.json( | ||
{ error: 'Failed to verify registration' }, | ||
{ status: 500 }, | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { | ||
Account, | ||
Address, | ||
type Keypair, | ||
Operation, | ||
SorobanRpc, | ||
StrKey, | ||
TransactionBuilder, | ||
hash, | ||
xdr, | ||
} from '@stellar/stellar-sdk' | ||
import { ENV } from '~/lib/passkey/env' | ||
|
||
const { RPC_URL, FACTORY_CONTRACT_ID, HORIZON_URL, NETWORK_PASSPHRASE } = ENV | ||
|
||
export async function handleDeploy( | ||
bundlerKey: Keypair, | ||
contractSalt: Buffer, | ||
publicKey?: Buffer, | ||
) { | ||
const rpc = new SorobanRpc.Server(RPC_URL) | ||
const deployee = StrKey.encodeContract( | ||
hash( | ||
xdr.HashIdPreimage.envelopeTypeContractId( | ||
new xdr.HashIdPreimageContractId({ | ||
networkId: hash(Buffer.from(NETWORK_PASSPHRASE, 'utf-8')), | ||
contractIdPreimage: | ||
xdr.ContractIdPreimage.contractIdPreimageFromAddress( | ||
new xdr.ContractIdPreimageFromAddress({ | ||
address: Address.fromString(FACTORY_CONTRACT_ID).toScAddress(), | ||
salt: contractSalt, | ||
}), | ||
), | ||
}), | ||
).toXDR(), | ||
), | ||
) | ||
|
||
// This is a signup deploy vs a signin deploy. Look up if this contract has been already been deployed, otherwise fail | ||
if (!publicKey) { | ||
await rpc.getContractData( | ||
deployee, | ||
xdr.ScVal.scvLedgerKeyContractInstance(), | ||
) | ||
return deployee | ||
} | ||
|
||
const bundlerKeyAccount = await rpc | ||
.getAccount(bundlerKey.publicKey()) | ||
.then((res) => new Account(res.accountId(), res.sequenceNumber())) | ||
const simTxn = new TransactionBuilder(bundlerKeyAccount, { | ||
fee: '100', | ||
networkPassphrase: NETWORK_PASSPHRASE, | ||
}) | ||
.addOperation( | ||
Operation.invokeContractFunction({ | ||
contract: FACTORY_CONTRACT_ID, | ||
function: 'deploy', | ||
args: [xdr.ScVal.scvBytes(contractSalt), xdr.ScVal.scvBytes(publicKey)], | ||
}), | ||
) | ||
.setTimeout(0) | ||
.build() | ||
|
||
const sim = await rpc.simulateTransaction(simTxn) | ||
|
||
if ( | ||
SorobanRpc.Api.isSimulationError(sim) || | ||
SorobanRpc.Api.isSimulationRestore(sim) | ||
) | ||
throw sim | ||
|
||
const transaction = SorobanRpc.assembleTransaction(simTxn, sim) | ||
.setTimeout(0) | ||
.build() | ||
|
||
transaction.sign(bundlerKey) | ||
|
||
// TODO failure here is resulting in sp:deployee undefined | ||
// TODO handle archived entries | ||
|
||
const txResp = await ( | ||
await fetch(`${HORIZON_URL}/transactions`, { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | ||
body: new URLSearchParams({ tx: transaction.toXDR() }), | ||
}) | ||
).json() | ||
|
||
if (txResp.successful) return deployee | ||
|
||
throw txResp | ||
} |
Oops, something went wrong.