2022-12-05 20:14:56 +00:00
|
|
|
import { getSigningKey } from 'wildebeest/backend/src/mastodon/account'
|
|
|
|
import * as oauth_authorize from 'wildebeest/functions/oauth/authorize'
|
|
|
|
import * as first_login from 'wildebeest/functions/first-login'
|
|
|
|
import * as oauth_token from 'wildebeest/functions/oauth/token'
|
2023-01-06 09:06:16 +00:00
|
|
|
import { isUrlValid, makeDB, assertCORS, assertJSON, createTestClient } from '../utils'
|
2022-12-05 20:14:56 +00:00
|
|
|
import { TEST_JWT, ACCESS_CERTS } from '../test-data'
|
|
|
|
import { strict as assert } from 'node:assert/strict'
|
2023-01-23 12:11:54 +00:00
|
|
|
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
|
2022-12-05 20:14:56 +00:00
|
|
|
|
|
|
|
const userKEK = 'test_kek3'
|
|
|
|
const accessDomain = 'access.com'
|
|
|
|
const accessAud = 'abcd'
|
|
|
|
|
|
|
|
describe('Mastodon APIs', () => {
|
|
|
|
describe('oauth', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
globalThis.fetch = async (input: RequestInfo) => {
|
|
|
|
if (input === 'https://' + accessDomain + '/cdn-cgi/access/certs') {
|
|
|
|
return new Response(JSON.stringify(ACCESS_CERTS))
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input === 'https://' + accessDomain + '/cdn-cgi/access/get-identity') {
|
|
|
|
return new Response(
|
|
|
|
JSON.stringify({
|
|
|
|
email: 'some@cloudflare.com',
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error('unexpected request to ' + input)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
test('authorize missing params', async () => {
|
|
|
|
const db = await makeDB()
|
|
|
|
|
|
|
|
let req = new Request('https://example.com/oauth/authorize')
|
2023-01-30 11:28:56 +00:00
|
|
|
let res = await oauth_authorize.handleRequestPost(req, db, userKEK, accessDomain, accessAud)
|
2023-02-06 14:33:04 +00:00
|
|
|
assert.equal(res.status, 401)
|
|
|
|
|
|
|
|
const headers = {
|
|
|
|
'Cf-Access-Jwt-Assertion': TEST_JWT,
|
|
|
|
}
|
|
|
|
|
|
|
|
req = new Request('https://example.com/oauth/authorize', { headers })
|
|
|
|
res = await oauth_authorize.handleRequestPost(req, db, userKEK, accessDomain, accessAud)
|
2022-12-05 20:14:56 +00:00
|
|
|
assert.equal(res.status, 400)
|
|
|
|
|
2023-02-06 14:33:04 +00:00
|
|
|
req = new Request('https://example.com/oauth/authorize?scope=foobar', { headers })
|
2023-01-30 11:28:56 +00:00
|
|
|
res = await oauth_authorize.handleRequestPost(req, db, userKEK, accessDomain, accessAud)
|
2022-12-05 20:14:56 +00:00
|
|
|
assert.equal(res.status, 400)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('authorize unsupported response_type', async () => {
|
|
|
|
const db = await makeDB()
|
|
|
|
|
2023-02-06 14:33:04 +00:00
|
|
|
const headers = {
|
|
|
|
'Cf-Access-Jwt-Assertion': TEST_JWT,
|
|
|
|
}
|
|
|
|
|
2022-12-05 20:14:56 +00:00
|
|
|
const params = new URLSearchParams({
|
|
|
|
redirect_uri: 'https://example.com',
|
|
|
|
response_type: 'hein',
|
|
|
|
client_id: 'client_id',
|
|
|
|
})
|
|
|
|
|
2023-02-06 14:33:04 +00:00
|
|
|
const req = new Request('https://example.com/oauth/authorize?' + params, { headers })
|
2023-01-30 11:28:56 +00:00
|
|
|
const res = await oauth_authorize.handleRequestPost(req, db, userKEK, accessDomain, accessAud)
|
2022-12-05 20:14:56 +00:00
|
|
|
assert.equal(res.status, 400)
|
|
|
|
})
|
|
|
|
|
|
|
|
test("authorize redirect_uri doesn't match client redirect_uris", async () => {
|
|
|
|
const db = await makeDB()
|
|
|
|
const client = await createTestClient(db, 'https://redirect.com')
|
|
|
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
redirect_uri: 'https://example.com/a',
|
|
|
|
response_type: 'code',
|
|
|
|
client_id: client.id,
|
|
|
|
})
|
|
|
|
|
|
|
|
const headers = { 'Cf-Access-Jwt-Assertion': TEST_JWT }
|
|
|
|
|
|
|
|
const req = new Request('https://example.com/oauth/authorize?' + params, {
|
|
|
|
headers,
|
|
|
|
})
|
2023-01-30 11:28:56 +00:00
|
|
|
const res = await oauth_authorize.handleRequestPost(req, db, userKEK, accessDomain, accessAud)
|
2022-12-05 20:14:56 +00:00
|
|
|
assert.equal(res.status, 403)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('authorize redirects with code on success and show first login', async () => {
|
|
|
|
const db = await makeDB()
|
|
|
|
const client = await createTestClient(db)
|
|
|
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
redirect_uri: client.redirect_uris,
|
|
|
|
response_type: 'code',
|
|
|
|
client_id: client.id,
|
2023-01-31 22:55:48 +00:00
|
|
|
state: 'mock-state',
|
2022-12-05 20:14:56 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
const headers = { 'Cf-Access-Jwt-Assertion': TEST_JWT }
|
|
|
|
|
|
|
|
const req = new Request('https://example.com/oauth/authorize?' + params, {
|
|
|
|
headers,
|
|
|
|
})
|
2023-01-30 11:28:56 +00:00
|
|
|
const res = await oauth_authorize.handleRequestPost(req, db, userKEK, accessDomain, accessAud)
|
2022-12-05 20:14:56 +00:00
|
|
|
assert.equal(res.status, 302)
|
|
|
|
|
|
|
|
const location = new URL(res.headers.get('location') || '')
|
|
|
|
assert.equal(
|
|
|
|
location.searchParams.get('redirect_uri'),
|
2023-01-31 22:55:48 +00:00
|
|
|
encodeURIComponent(`${client.redirect_uris}?code=${client.id}.${TEST_JWT}&state=mock-state`)
|
2022-12-05 20:14:56 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// actor isn't created yet
|
2023-01-11 15:45:07 +00:00
|
|
|
const { count } = await db.prepare('SELECT count(*) as count FROM actors').first<{ count: number }>()
|
2022-12-05 20:14:56 +00:00
|
|
|
assert.equal(count, 0)
|
|
|
|
})
|
|
|
|
|
2023-01-20 15:03:52 +00:00
|
|
|
test('first login is protected by Access', async () => {
|
|
|
|
const db = await makeDB()
|
|
|
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
redirect_uri: 'https://redirect.com/a',
|
|
|
|
})
|
|
|
|
|
|
|
|
const formData = new FormData()
|
|
|
|
formData.set('username', 'username')
|
|
|
|
formData.set('name', 'name')
|
|
|
|
|
|
|
|
const req = new Request('https://example.com/first-login?' + params, {
|
|
|
|
method: 'POST',
|
|
|
|
body: formData,
|
|
|
|
})
|
|
|
|
const res = await first_login.handlePostRequest(req, db, userKEK, accessDomain, accessAud)
|
|
|
|
assert.equal(res.status, 401)
|
|
|
|
})
|
|
|
|
|
2022-12-05 20:14:56 +00:00
|
|
|
test('first login creates the user and redirects', async () => {
|
|
|
|
const db = await makeDB()
|
|
|
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
redirect_uri: 'https://redirect.com/a',
|
|
|
|
})
|
|
|
|
|
|
|
|
const formData = new FormData()
|
|
|
|
formData.set('username', 'username')
|
|
|
|
formData.set('name', 'name')
|
|
|
|
|
|
|
|
const req = new Request('https://example.com/first-login?' + params, {
|
|
|
|
method: 'POST',
|
|
|
|
body: formData,
|
2023-01-20 15:03:52 +00:00
|
|
|
headers: {
|
|
|
|
cookie: `CF_Authorization=${TEST_JWT}`,
|
|
|
|
},
|
2022-12-05 20:14:56 +00:00
|
|
|
})
|
2023-01-20 15:03:52 +00:00
|
|
|
const res = await first_login.handlePostRequest(req, db, userKEK, accessDomain, accessAud)
|
2022-12-05 20:14:56 +00:00
|
|
|
assert.equal(res.status, 302)
|
|
|
|
|
|
|
|
const location = res.headers.get('location')
|
|
|
|
assert.equal(location, 'https://redirect.com/a')
|
|
|
|
|
2023-01-11 15:45:07 +00:00
|
|
|
const actor = await db
|
|
|
|
.prepare('SELECT * FROM actors')
|
|
|
|
.first<{ properties: string; email: string; id: string } & Actor>()
|
2022-12-05 20:14:56 +00:00
|
|
|
const properties = JSON.parse(actor.properties)
|
|
|
|
|
2023-01-20 15:03:52 +00:00
|
|
|
assert.equal(actor.email, 'sven@cloudflare.com')
|
2022-12-05 20:14:56 +00:00
|
|
|
assert.equal(properties.preferredUsername, 'username')
|
|
|
|
assert.equal(properties.name, 'name')
|
|
|
|
assert(isUrlValid(actor.id))
|
|
|
|
// ensure that we generate a correct key pairs for the user
|
2023-01-11 15:45:07 +00:00
|
|
|
assert((await getSigningKey(userKEK, db, actor as Actor)) instanceof CryptoKey)
|
2022-12-05 20:14:56 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
test('token error on unknown client', async () => {
|
|
|
|
const db = await makeDB()
|
2023-01-10 10:09:19 +00:00
|
|
|
const body = new URLSearchParams({ code: 'some-code' })
|
2022-12-05 20:14:56 +00:00
|
|
|
|
|
|
|
const req = new Request('https://example.com/oauth/token', {
|
|
|
|
method: 'POST',
|
2023-01-10 10:09:19 +00:00
|
|
|
body,
|
2022-12-05 20:14:56 +00:00
|
|
|
})
|
|
|
|
const res = await oauth_token.handleRequest(db, req)
|
|
|
|
assert.equal(res.status, 403)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('token returns auth infos', async () => {
|
|
|
|
const db = await makeDB()
|
|
|
|
const testScope = 'test abcd'
|
|
|
|
const client = await createTestClient(db, 'https://localhost', testScope)
|
|
|
|
|
2023-01-10 10:09:19 +00:00
|
|
|
const body = new URLSearchParams({
|
2022-12-05 20:14:56 +00:00
|
|
|
code: client.id + '.some-code',
|
2023-01-10 10:09:19 +00:00
|
|
|
})
|
2022-12-05 20:14:56 +00:00
|
|
|
|
|
|
|
const req = new Request('https://example.com/oauth/token', {
|
|
|
|
method: 'POST',
|
2023-01-10 10:09:19 +00:00
|
|
|
body,
|
2022-12-05 20:14:56 +00:00
|
|
|
})
|
|
|
|
const res = await oauth_token.handleRequest(db, req)
|
|
|
|
assert.equal(res.status, 200)
|
|
|
|
assertCORS(res)
|
|
|
|
assertJSON(res)
|
|
|
|
|
|
|
|
const data = await res.json<any>()
|
2023-01-10 10:09:19 +00:00
|
|
|
assert.equal(data.access_token, client.id + '.some-code')
|
2022-12-05 20:14:56 +00:00
|
|
|
assert.equal(data.scope, testScope)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('token handles empty code', async () => {
|
|
|
|
const db = await makeDB()
|
2023-01-10 10:09:19 +00:00
|
|
|
const body = new URLSearchParams({ code: '' })
|
2022-12-05 20:14:56 +00:00
|
|
|
|
|
|
|
const req = new Request('https://example.com/oauth/token', {
|
|
|
|
method: 'POST',
|
2023-01-10 10:09:19 +00:00
|
|
|
body,
|
2022-12-05 20:14:56 +00:00
|
|
|
})
|
|
|
|
const res = await oauth_token.handleRequest(db, req)
|
|
|
|
assert.equal(res.status, 401)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('token returns CORS', async () => {
|
|
|
|
const db = await makeDB()
|
|
|
|
const req = new Request('https://example.com/oauth/token', {
|
|
|
|
method: 'OPTIONS',
|
|
|
|
})
|
|
|
|
const res = await oauth_token.handleRequest(db, req)
|
|
|
|
assert.equal(res.status, 200)
|
|
|
|
assertCORS(res)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('authorize returns CORS', async () => {
|
|
|
|
const db = await makeDB()
|
|
|
|
const req = new Request('https://example.com/oauth/authorize', {
|
|
|
|
method: 'OPTIONS',
|
|
|
|
})
|
2023-01-30 11:28:56 +00:00
|
|
|
const res = await oauth_authorize.handleRequestPost(req, db, userKEK, accessDomain, accessAud)
|
2022-12-05 20:14:56 +00:00
|
|
|
assert.equal(res.status, 200)
|
|
|
|
assertCORS(res)
|
|
|
|
})
|
2023-02-15 18:31:20 +00:00
|
|
|
|
|
|
|
test('token handles code in URL', async () => {
|
|
|
|
const db = await makeDB()
|
|
|
|
const client = await createTestClient(db, 'https://localhost')
|
|
|
|
|
|
|
|
const code = client.id + '.a'
|
|
|
|
|
|
|
|
const req = new Request('https://example.com/oauth/token?code=' + code, {
|
|
|
|
method: 'POST',
|
|
|
|
headers: {
|
|
|
|
'content-type': 'application/json',
|
|
|
|
},
|
|
|
|
body: '',
|
|
|
|
})
|
|
|
|
const res = await oauth_token.handleRequest(db, req)
|
|
|
|
assert.equal(res.status, 200)
|
|
|
|
|
|
|
|
const data = await res.json<any>()
|
|
|
|
assert.equal(data.access_token, code)
|
|
|
|
})
|
2022-12-05 20:14:56 +00:00
|
|
|
})
|
|
|
|
})
|