Skip to content

Commit

Permalink
Merge branch 'kindfi-org:develop' into fix/open-redirect-validation
Browse files Browse the repository at this point in the history
  • Loading branch information
sebas11042 authored Feb 2, 2025
2 parents 7e110b6 + b606803 commit 79f5e54
Show file tree
Hide file tree
Showing 13 changed files with 1,122 additions and 81 deletions.
132 changes: 132 additions & 0 deletions apps/web/app/api/passkey/README.md
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 apps/web/app/api/passkey/generate-authentication-options/route.ts
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 apps/web/app/api/passkey/generate-registration-options/route.ts
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 },
)
}
}
25 changes: 25 additions & 0 deletions apps/web/app/api/passkey/verify-authentication/route.ts
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 },
)
}
}
25 changes: 25 additions & 0 deletions apps/web/app/api/passkey/verify-registration/route.ts
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 },
)
}
}
93 changes: 93 additions & 0 deletions apps/web/lib/passkey/deploy.ts
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
}
Loading

0 comments on commit 79f5e54

Please sign in to comment.