api: serialize refresh and revoke per grant, hide expired refresh tokens, refuse non-string token fields547d83e2 days ago305 lines · 21.9 KB | 1 | import { type Database, schema } from '@ghost/db'; |
| 2 | import type { INestApplication |
| 3 | import { AuthService } |
| 4 | import { eq } |
| 5 | import request from 'supertest |
| 6 | import { afterAll, beforeAll |
| 7 | |
| 8 | import { DATABASE } |
| 9 | import type { Auth |
| 10 | import { hasBackends, scopedKey |
| 11 | |
| 12 | const GH = ' |
| 13 | const DEVICE_GRANT = ' |
| 14 | |
| 15 | describe.skipIf(!hasBackends |
| 16 | let app: |
| 17 | let db: |
| 18 | let owner: |
| 19 | let other: |
| 20 | const stamp = |
| 21 | const api = |
| 22 | const create = |
| 23 | |
| 24 | beforeAll(async () |
| 25 | ({ app } |
| 26 | db = app. |
| 27 | owner = await |
| 28 | other = await |
| 29 | }); |
| 30 | |
| 31 | afterAll(async () |
| 32 | await app?.close |
| 33 | }); |
| 34 | |
| 35 | it('shows the client secret once, on create, and never on read', |
| 36 | const created = |
| 37 | expect(created.body |
| 38 | expect(created.body |
| 39 | expect(created.body |
| 40 | |
| 41 | const one = |
| 42 | expect(one.body |
| 43 | expect(one.body |
| 44 | const list = |
| 45 | expect(list.body |
| 46 | expect(JSON.stringify |
| 47 | |
| 48 | const [row] |
| 49 | expect(row?.clientSecret |
| 50 | expect(row?.clientSecret |
| 51 | }); |
| 52 | |
| 53 | it('updates an app and turns its device flow on', |
| 54 | const { body |
| 55 | const updated = |
| 56 | expect(updated.body |
| 57 | await api().post |
| 58 | |
| 59 | const enabled = |
| 60 | expect(enabled.body |
| 61 | }); |
| 62 | |
| 63 | it('rotates the secret, replacing the stored one', |
| 64 | const { body |
| 65 | const before = |
| 66 | const rotated = |
| 67 | expect(rotated.body |
| 68 | expect(rotated.body |
| 69 | const after = |
| 70 | expect(after[0] |
| 71 | }); |
| 72 | |
| 73 | it("hides another user's app behind 404", |
| 74 | const { body |
| 75 | await api().get |
| 76 | await api().patch |
| 77 | await api().post |
| 78 | await api().delete |
| 79 | const list = |
| 80 | expect(list.body |
| 81 | }); |
| 82 | |
| 83 | it('refuses to change gh', |
| 84 | await api().patch |
| 85 | await api().post |
| 86 | await api().delete |
| 87 | const [row] |
| 88 | expect(row?.name |
| 89 | }); |
| 90 | |
| 91 | it('deletes an app with the keys it minted for every user, and nothing else', |
| 92 | const { body |
| 93 | const auth = |
| 94 | await auth(owner |
| 95 | await auth(other |
| 96 | await auth(owner |
| 97 | await api().delete |
| 98 | await api().get |
| 99 | const keys = |
| 100 | const ids = |
| 101 | expect(ids).not |
| 102 | expect(ids).not |
| 103 | expect(ids).toContain |
| 104 | }); |
| 105 | |
| 106 | it('keeps a description and several callbacks, production and local alike', |
| 107 | const callbackUrls = |
| 108 | const { body |
| 109 | expect(body).toMatchObject |
| 110 | const updated = |
| 111 | expect(updated.body |
| 112 | const described = |
| 113 | expect(described.body |
| 114 | }); |
| 115 | |
| 116 | it('refuses an empty, oversized or partly invalid callback list', |
| 117 | await create(owner |
| 118 | await create(owner |
| 119 | await create(owner |
| 120 | }); |
| 121 | |
| 122 | it('sets and removes a logo, which the consent page sees', |
| 123 | const { body |
| 124 | const png = |
| 125 | const uploaded = |
| 126 | expect(uploaded.body |
| 127 | const described = |
| 128 | expect(described.body |
| 129 | await api().put |
| 130 | await api().put |
| 131 | await api().delete |
| 132 | expect((await api |
| 133 | }); |
| 134 | |
| 135 | it('marks gh verified, and no owner can mark their own app', |
| 136 | const gh = |
| 137 | expect(gh.body |
| 138 | const { body |
| 139 | await api().patch |
| 140 | const described = |
| 141 | expect(described.body |
| 142 | }); |
| 143 | |
| 144 | it('accepts only an https or localhost callback', |
| 145 | await create(owner |
| 146 | await create(owner |
| 147 | await create(owner |
| 148 | await create(owner |
| 149 | }); |
| 150 | |
| 151 | it('turns expiring user tokens on and off, off by default', |
| 152 | const { body |
| 153 | expect(body.expireUserTokens |
| 154 | const on = |
| 155 | expect(on.body |
| 156 | const off = |
| 157 | expect(off.body |
| 158 | const created = |
| 159 | expect(created.body |
| 160 | await api().patch |
| 161 | await api().patch |
| 162 | }); |
| 163 | |
| 164 | it('refuses an API key of any scope', |
| 165 | const key = |
| 166 | await api().get |
| 167 | await api().get |
| 168 | }); |
| 169 | |
| 170 | describe('authorized apps', |
| 171 | const key = |
| 172 | db.insert(schema |
| 173 | const ids = |
| 174 | |
| 175 | it('lists the apps holding a key for me, with their scopes', |
| 176 | const a = |
| 177 | await key(`la1 |
| 178 | await key(`la2 |
| 179 | const list = |
| 180 | const entry = |
| 181 | expect(entry).toMatchObject |
| 182 | expect(entry.authorizedAt |
| 183 | const theirs = |
| 184 | expect(theirs.body |
| 185 | }); |
| 186 | |
| 187 | it("revokes only my keys for that app, and its consent", |
| 188 | const a = |
| 189 | const b = |
| 190 | await key(`ra-mine |
| 191 | await key(`ra-theirs |
| 192 | await key(`rb-mine |
| 193 | await key(`r-ui |
| 194 | await db.insert |
| 195 | |
| 196 | await api().delete |
| 197 | const left = |
| 198 | expect(left).not |
| 199 | expect(left).toEqual |
| 200 | expect(await db |
| 201 | await api().delete |
| 202 | }); |
| 203 | |
| 204 | it('keeps an app whose key expired but whose refresh token lives, and revoking it deletes the refresh token', |
| 205 | const a = |
| 206 | await db.insert |
| 207 | const list = |
| 208 | expect(list.body |
| 209 | await api().delete |
| 210 | expect(await db |
| 211 | await api().delete |
| 212 | }); |
| 213 | |
| 214 | it('drops an app once its last refresh token has expired', |
| 215 | const a = |
| 216 | await db.insert |
| 217 | const list = |
| 218 | expect(list.body |
| 219 | }); |
| 220 | |
| 221 | it('merges the scopes of keys and refresh tokens into one entry', |
| 222 | const a = |
| 223 | await key(`mk |
| 224 | await db.insert |
| 225 | const list = |
| 226 | expect(list.body |
| 227 | }); |
| 228 | |
| 229 | it('shows gh after the device flow, and revoking it locks gh out', |
| 230 | const form = |
| 231 | const code = |
| 232 | await api().get |
| 233 | await api().post |
| 234 | const token = |
| 235 | await api().get |
| 236 | |
| 237 | const list = |
| 238 | expect(list.body |
| 239 | await api().delete |
| 240 | await api().get |
| 241 | }); |
| 242 | }); |
| 243 | |
| 244 | describe('organization apps', |
| 245 | const auth = |
| 246 | const org = |
| 247 | let organizationId: |
| 248 | let admin: |
| 249 | let member: |
| 250 | const createFor = |
| 251 | |
| 252 | beforeAll(async () |
| 253 | admin = await |
| 254 | member = await |
| 255 | organizationId = (await |
| 256 | await auth().addMember |
| 257 | await auth().addMember |
| 258 | }); |
| 259 | |
| 260 | it('files the app under the organization, not the user who made it', |
| 261 | const { body |
| 262 | expect(body.clientSecret |
| 263 | const [row] |
| 264 | expect(row).toEqual |
| 265 | |
| 266 | const listed = |
| 267 | expect(listed.body |
| 268 | const mine = |
| 269 | expect(mine.body |
| 270 | const described = |
| 271 | expect(described.body |
| 272 | }); |
| 273 | |
| 274 | it('lets any admin manage it, through the same routes as a personal app', |
| 275 | const { body |
| 276 | await api().get |
| 277 | const updated = |
| 278 | expect(updated.body |
| 279 | const rotated = |
| 280 | expect(rotated.body |
| 281 | await api().delete |
| 282 | await api().get |
| 283 | }); |
| 284 | |
| 285 | it('keeps members below admin and outsiders out', |
| 286 | const { body |
| 287 | await createFor(member |
| 288 | await api().get |
| 289 | await api().patch |
| 290 | await api().post |
| 291 | await api().delete |
| 292 | await createFor(other |
| 293 | }); |
| 294 | |
| 295 | it('goes, with the keys it minted, when the organization is deleted', |
| 296 | const slug = |
| 297 | const gone = |
| 298 | const { body |
| 299 | await db.insert |
| 300 | await auth().deleteOrganization |
| 301 | expect(await db |
| 302 | expect(await db |
| 303 | }); |
| 304 | }); |
| 305 | }); |
}
from
'
@nestjs/common
'
;
from
'
@thallesp/nestjs-better-auth
'
;
from
'
drizzle-orm
'
;
'
;
,
describe
,
expect
,
it
}
from
'
vitest
'
;
from
'
../src/database/database.module.js
'
;
}
from
'
../src/lib/auth.js
'
;
,
signUp
,
startApp
}
from
'
./harness.js
'
;
178c6fc778ccc68e1d6a
'
;
urn:ietf:params:oauth:grant-type:device_code
'
;
)(
'
OAuth apps
'
,
()
=>
{
INestApplication
;
Database
;
{
cookie
:
string
;
key
:
string
;
userId
:
string
}
;
{
cookie
:
string
;
key
:
string
;
userId
:
string
}
;
Date
.
now
()
;
()
=>
request
(
app
.
getHttpServer
())
;
(
cookie
:
string
,
body
:
object
=
{})
=>
api
()
.
post
(
'
/api/oauth-apps
'
)
.
set
(
'
cookie
'
,
cookie
)
.
send
(
{
name
:
'
Scoped Tool
'
,
homepageUrl
:
'
https://tool.example
'
,
callbackUrls
:
[
'
https://tool.example/callback
'
]
,
...
body
}
)
;
=>
{
=
await
startApp
())
;
get
<
Database
>
(
DATABASE
)
;
signUp
(
app
,
`
oaapps
${
stamp
}
`
)
;
signUp
(
app
,
`
oaother
${
stamp
}
`
)
;
=>
{
()
;
async
()
=>
{
await
create
(
owner
.
cookie
)
.
expect
(
201
)
;
)
.
toMatchObject
(
{
name
:
'
Scoped Tool
'
,
homepageUrl
:
'
https://tool.example
'
,
callbackUrls
:
[
'
https://tool.example/callback
'
]
,
deviceFlowEnabled
:
false
}
)
;
.
clientId
)
.
toBeTruthy
()
;
.
clientSecret
)
.
toEqual
(
expect
.
any
(
String
))
;
await
api
()
.
get
(
`
/api/oauth-apps/
${
created
.
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
)
;
.
clientSecret
)
.
toBeUndefined
()
;
)
.
toMatchObject
(
{
clientId
:
created
.
body
.
clientId
,
name
:
'
Scoped Tool
'
}
)
;
await
api
()
.
get
(
'
/api/oauth-apps
'
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
)
;
.
apps
.
map
(
(
each
:
{
clientId
:
string
})
=>
each
.
clientId
))
.
toContain
(
created
.
body
.
clientId
)
;
(
list
.
body
))
.
not
.
toContain
(
created
.
body
.
clientSecret
)
;
=
await
db
.
select
(
{
clientSecret
:
schema
.
oauthClient
.
clientSecret
}
)
.
from
(
schema
.
oauthClient
)
.
where
(
eq
(
schema
.
oauthClient
.
clientId
,
created
.
body
.
clientId
))
;
)
.
toBeTruthy
()
;
)
.
not
.
toBe
(
created
.
body
.
clientSecret
)
;
async
()
=>
{
}
=
await
create
(
owner
.
cookie
)
.
expect
(
201
)
;
await
api
()
.
patch
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
send
(
{
name
:
'
Renamed
'
,
callbackUrls
:
[
'
http://localhost:4000/cb
'
]
,
deviceFlowEnabled
:
true
}
)
.
expect
(
200
)
;
)
.
toMatchObject
(
{
name
:
'
Renamed
'
,
callbackUrls
:
[
'
http://localhost:4000/cb
'
]
,
deviceFlowEnabled
:
true
,
homepageUrl
:
'
https://tool.example
'
}
)
;
(
'
/login/device/code
'
)
.
type
(
'
form
'
)
.
send
(
{
client_id
:
body
.
clientId
,
scope
:
'
repo
'
}
)
.
expect
(
200
)
;
await
create
(
owner
.
cookie
,
{
deviceFlowEnabled
:
true
}
)
.
expect
(
201
)
;
.
deviceFlowEnabled
)
.
toBe
(
true
)
;
async
()
=>
{
}
=
await
create
(
owner
.
cookie
)
.
expect
(
201
)
;
await
db
.
select
(
{
clientSecret
:
schema
.
oauthClient
.
clientSecret
}
)
.
from
(
schema
.
oauthClient
)
.
where
(
eq
(
schema
.
oauthClient
.
clientId
,
body
.
clientId
))
;
await
api
()
.
post
(
`
/api/oauth-apps/
${
body
.
clientId
}
/secret
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
201
)
;
.
clientSecret
)
.
toEqual
(
expect
.
any
(
String
))
;
.
clientSecret
)
.
not
.
toBe
(
body
.
clientSecret
)
;
await
db
.
select
(
{
clientSecret
:
schema
.
oauthClient
.
clientSecret
}
)
.
from
(
schema
.
oauthClient
)
.
where
(
eq
(
schema
.
oauthClient
.
clientId
,
body
.
clientId
))
;
?.
clientSecret
)
.
not
.
toBe
(
before
[
0
]
?.
clientSecret
)
;
async
()
=>
{
}
=
await
create
(
owner
.
cookie
)
.
expect
(
201
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
expect
(
404
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
send
(
{
name
:
'
Mine now
'
}
)
.
expect
(
404
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
/secret
`
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
expect
(
404
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
expect
(
404
)
;
await
api
()
.
get
(
'
/api/oauth-apps
'
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
expect
(
200
)
;
.
apps
)
.
toEqual
([])
;
async
()
=>
{
(
`
/api/oauth-apps/
${
GH
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
send
(
{
name
:
'
Not gh
'
}
)
.
expect
(
403
)
;
(
`
/api/oauth-apps/
${
GH
}
/secret
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
403
)
;
(
`
/api/oauth-apps/
${
GH
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
403
)
;
=
await
db
.
select
(
{
name
:
schema
.
oauthClient
.
name
}
)
.
from
(
schema
.
oauthClient
)
.
where
(
eq
(
schema
.
oauthClient
.
clientId
,
GH
))
;
)
.
toBe
(
'
GitHub CLI
'
)
;
async
()
=>
{
}
=
await
create
(
owner
.
cookie
)
.
expect
(
201
)
;
(
userId
:
string
,
clientId
:
string
)
=>
db
.
insert
(
schema
.
apikey
)
.
values
(
{
id
:
`
k
${
clientId
}${
userId
}
`
,
referenceId
:
userId
,
key
:
`
hash
${
clientId
}${
userId
}
`
,
name
:
'
k
'
,
metadata
:
JSON
.
stringify
(
{
oauthClientId
:
clientId
}
)
,
createdAt
:
new
Date
()
,
updatedAt
:
new
Date
()
}
)
;
.
userId
,
body
.
clientId
)
;
.
userId
,
body
.
clientId
)
;
.
userId
,
GH
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
204
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
404
)
;
await
db
.
select
(
{
id
:
schema
.
apikey
.
id
}
)
.
from
(
schema
.
apikey
)
;
keys
.
map
(
(
key
)
=>
key
.
id
)
;
.
toContain
(
`
k
${
body
.
clientId
}${
owner
.
userId
}
`
)
;
.
toContain
(
`
k
${
body
.
clientId
}${
other
.
userId
}
`
)
;
(
`
k
${
GH
}${
owner
.
userId
}
`
)
;
async
()
=>
{
[
'
https://tool.example/callback
'
,
'
http://localhost:4000/cb
'
]
;
}
=
await
create
(
owner
.
cookie
,
{
description
:
'
Ships releases.
'
,
callbackUrls
}
)
.
expect
(
201
)
;
(
{
description
:
'
Ships releases.
'
,
callbackUrls
,
logoUrl
:
null
}
)
;
await
api
()
.
patch
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
send
(
{
description
:
'
Ships faster.
'
}
)
.
expect
(
200
)
;
)
.
toMatchObject
(
{
description
:
'
Ships faster.
'
,
callbackUrls
,
deviceFlowEnabled
:
false
}
)
;
await
api
()
.
get
(
'
/api/oauth-apps/authorize
'
)
.
query
(
{
client_id
:
body
.
clientId
}
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
expect
(
200
)
;
)
.
toMatchObject
(
{
description
:
'
Ships faster.
'
,
logoUrl
:
null
}
)
;
async
()
=>
{
.
cookie
,
{
callbackUrls
:
[]
}
)
.
expect
(
400
)
;
.
cookie
,
{
callbackUrls
:
Array
.
from
(
{
length
:
11
}
,
(
_
,
index
)
=>
`
https://tool.example/
${
index
}
`
)
}
)
.
expect
(
400
)
;
.
cookie
,
{
callbackUrls
:
[
'
https://tool.example/ok
'
,
'
http://tool.example/bad
'
]
}
)
.
expect
(
400
)
;
async
()
=>
{
}
=
await
create
(
owner
.
cookie
)
.
expect
(
201
)
;
Buffer
.
from
(
'
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==
'
,
'
base64
'
)
;
await
api
()
.
put
(
`
/api/oauth-apps/
${
body
.
clientId
}
/logo
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
set
(
'
content-type
'
,
'
image/png
'
)
.
send
(
png
)
.
expect
(
200
)
;
.
url
)
.
toMatch
(
/
\.
png
$
/
)
;
await
api
()
.
get
(
'
/api/oauth-apps/authorize
'
)
.
query
(
{
client_id
:
body
.
clientId
}
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
)
;
.
logoUrl
)
.
toBe
(
uploaded
.
body
.
url
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
/logo
`
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
set
(
'
content-type
'
,
'
image/png
'
)
.
send
(
png
)
.
expect
(
404
)
;
(
`
/api/oauth-apps/
${
GH
}
/logo
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
set
(
'
content-type
'
,
'
image/png
'
)
.
send
(
png
)
.
expect
(
403
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
/logo
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
204
)
;
()
.
get
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
))
.
body
.
logoUrl
)
.
toBeNull
()
;
async
()
=>
{
await
api
()
.
get
(
'
/api/oauth-apps/authorize
'
)
.
query
(
{
client_id
:
GH
}
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
)
;
.
verified
)
.
toBe
(
true
)
;
}
=
await
create
(
owner
.
cookie
,
{
verified
:
true
,
metadata
:
{
verified
:
true
}
}
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
send
(
{
verified
:
true
,
metadata
:
{
verified
:
true
}
}
)
;
await
api
()
.
get
(
'
/api/oauth-apps/authorize
'
)
.
query
(
{
client_id
:
body
.
clientId
}
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
expect
(
200
)
;
.
verified
)
.
toBe
(
false
)
;
async
()
=>
{
.
cookie
,
{
callbackUrls
:
[
'
http://tool.example/callback
'
]
}
)
.
expect
(
400
)
;
.
cookie
,
{
callbackUrls
:
[
'
javascript:alert(1)
'
]
}
)
.
expect
(
400
)
;
.
cookie
,
{
callbackUrls
:
[
'
https://localhost/callback
'
]
}
)
.
expect
(
400
)
;
.
cookie
,
{
callbackUrls
:
[
'
http://127.0.0.1:8080/cb
'
]
}
)
.
expect
(
201
)
;
async
()
=>
{
}
=
await
create
(
owner
.
cookie
)
.
expect
(
201
)
;
)
.
toBe
(
false
)
;
await
api
()
.
patch
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
send
(
{
expireUserTokens
:
true
}
)
.
expect
(
200
)
;
)
.
toMatchObject
(
{
expireUserTokens
:
true
,
deviceFlowEnabled
:
false
,
name
:
'
Scoped Tool
'
}
)
;
await
api
()
.
patch
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
send
(
{
expireUserTokens
:
false
}
)
.
expect
(
200
)
;
.
expireUserTokens
)
.
toBe
(
false
)
;
await
create
(
owner
.
cookie
,
{
expireUserTokens
:
true
}
)
.
expect
(
201
)
;
.
expireUserTokens
)
.
toBe
(
true
)
;
(
`
/api/oauth-apps/
${
GH
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
send
(
{
expireUserTokens
:
true
}
)
.
expect
(
403
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
send
(
{
expireUserTokens
:
'
yes
'
}
)
.
expect
(
400
)
;
async
()
=>
{
await
scopedKey
(
app
,
owner
.
userId
,
[
'
repo
'
,
'
user
'
])
;
(
'
/api/oauth-apps
'
)
.
set
(
'
authorization
'
,
`
Bearer
${
key
}
`
)
.
expect
(
401
)
;
(
'
/api/oauth-apps
'
)
.
set
(
'
x-api-key
'
,
owner
.
key
)
.
expect
(
401
)
;
()
=>
{
(
id
:
string
,
userId
:
string
,
clientId
:
string
|
null
,
scopes
:
string
[]
)
=>
.
apikey
)
.
values
(
{
id
,
referenceId
:
userId
,
key
:
`
hash-
${
id
}
`
,
name
:
'
k
'
,
permissions
:
JSON
.
stringify
(
{
scopes
}
)
,
metadata
:
clientId
?
JSON
.
stringify
(
{
oauthClientId
:
clientId
}
)
:
null
,
createdAt
:
new
Date
()
,
updatedAt
:
new
Date
()
}
)
;
async
()
=>
(
await
db
.
select
(
{
id
:
schema
.
apikey
.
id
}
)
.
from
(
schema
.
apikey
))
.
map
(
(
row
)
=>
row
.
id
)
;
async
()
=>
{
(
await
create
(
other
.
cookie
,
{
name
:
'
App A
'
}
)
.
expect
(
201
))
.
body
.
clientId
;
${
stamp
}
`
,
owner
.
userId
,
a
,
[
'
repo
'
])
;
${
stamp
}
`
,
owner
.
userId
,
a
,
[
'
gist
'
,
'
repo
'
])
;
await
api
()
.
get
(
'
/api/oauth-apps/authorized
'
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
)
;
list
.
body
.
apps
.
find
(
(
each
:
{
clientId
:
string
})
=>
each
.
clientId
===
a
)
;
(
{
clientId
:
a
,
name
:
'
App A
'
,
scopes
:
[
'
gist
'
,
'
repo
'
]
,
lastUsedAt
:
null
}
)
;
)
.
toEqual
(
expect
.
any
(
String
))
;
await
api
()
.
get
(
'
/api/oauth-apps/authorized
'
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
expect
(
200
)
;
.
apps
.
map
(
(
each
:
{
clientId
:
string
})
=>
each
.
clientId
))
.
not
.
toContain
(
a
)
;
async
()
=>
{
(
await
create
(
other
.
cookie
,
{
name
:
'
App A
'
}
)
.
expect
(
201
))
.
body
.
clientId
;
(
await
create
(
other
.
cookie
,
{
name
:
'
App B
'
}
)
.
expect
(
201
))
.
body
.
clientId
;
${
stamp
}
`
,
owner
.
userId
,
a
,
[
'
repo
'
])
;
${
stamp
}
`
,
other
.
userId
,
a
,
[
'
repo
'
])
;
${
stamp
}
`
,
owner
.
userId
,
b
,
[
'
repo
'
])
;
${
stamp
}
`
,
owner
.
userId
,
null
,
[])
;
(
schema
.
oauthConsent
)
.
values
(
{
id
:
`
c
${
stamp
}
`
,
clientId
:
a
,
userId
:
owner
.
userId
,
scopes
:
[
'
repo
'
]
,
createdAt
:
new
Date
()
,
updatedAt
:
new
Date
()
}
)
;
(
`
/api/oauth-apps/authorized/
${
a
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
204
)
;
await
ids
()
;
.
toContain
(
`
ra-mine
${
stamp
}
`
)
;
(
expect
.
arrayContaining
([
`
ra-theirs
${
stamp
}
`
,
`
rb-mine
${
stamp
}
`
,
`
r-ui
${
stamp
}
`
]))
;
.
select
()
.
from
(
schema
.
oauthConsent
)
.
where
(
eq
(
schema
.
oauthConsent
.
id
,
`
c
${
stamp
}
`
)))
.
toEqual
([])
;
(
`
/api/oauth-apps/authorized/
${
a
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
404
)
;
async
()
=>
{
(
await
create
(
other
.
cookie
,
{
name
:
'
App R
'
}
)
.
expect
(
201
))
.
body
.
clientId
;
(
schema
.
oauthAppRefreshToken
)
.
values
(
{
tokenHash
:
`
rt-
${
stamp
}
`
,
clientId
:
a
,
userId
:
owner
.
userId
,
scopes
:
[
'
read:org
'
,
'
repo
'
]
,
accessKeyId
:
'
gone
'
,
expiresAt
:
new
Date
(
Date
.
now
()
+
60_000
)
}
)
;
await
api
()
.
get
(
'
/api/oauth-apps/authorized
'
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
)
;
.
apps
.
find
(
(
each
:
{
clientId
:
string
})
=>
each
.
clientId
===
a
))
.
toMatchObject
(
{
name
:
'
App R
'
,
scopes
:
[
'
read:org
'
,
'
repo
'
]
,
lastUsedAt
:
null
,
authorizedAt
:
expect
.
any
(
String
)
}
)
;
(
`
/api/oauth-apps/authorized/
${
a
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
204
)
;
.
select
()
.
from
(
schema
.
oauthAppRefreshToken
)
.
where
(
eq
(
schema
.
oauthAppRefreshToken
.
clientId
,
a
)))
.
toEqual
([])
;
(
`
/api/oauth-apps/authorized/
${
a
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
404
)
;
async
()
=>
{
(
await
create
(
other
.
cookie
,
{
name
:
'
App X
'
}
)
.
expect
(
201
))
.
body
.
clientId
;
(
schema
.
oauthAppRefreshToken
)
.
values
(
{
tokenHash
:
`
rx-
${
stamp
}
`
,
clientId
:
a
,
userId
:
owner
.
userId
,
scopes
:
[
'
repo
'
]
,
accessKeyId
:
'
gone
'
,
expiresAt
:
new
Date
(
Date
.
now
()
-
1000
)
}
)
;
await
api
()
.
get
(
'
/api/oauth-apps/authorized
'
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
)
;
.
apps
.
map
(
(
each
:
{
clientId
:
string
})
=>
each
.
clientId
))
.
not
.
toContain
(
a
)
;
async
()
=>
{
(
await
create
(
other
.
cookie
,
{
name
:
'
App M
'
}
)
.
expect
(
201
))
.
body
.
clientId
;
${
stamp
}
`
,
owner
.
userId
,
a
,
[
'
repo
'
])
;
(
schema
.
oauthAppRefreshToken
)
.
values
(
{
tokenHash
:
`
rm-
${
stamp
}
`
,
clientId
:
a
,
userId
:
owner
.
userId
,
scopes
:
[
'
gist
'
,
'
repo
'
]
,
accessKeyId
:
`
mk
${
stamp
}
`
,
expiresAt
:
new
Date
(
Date
.
now
()
+
60_000
)
}
)
;
await
api
()
.
get
(
'
/api/oauth-apps/authorized
'
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
)
;
.
apps
.
filter
(
(
each
:
{
clientId
:
string
})
=>
each
.
clientId
===
a
))
.
toEqual
([
expect
.
objectContaining
(
{
scopes
:
[
'
gist
'
,
'
repo
'
]
}
)])
;
async
()
=>
{
(
text
:
string
)
=>
Object
.
fromEntries
(
new
URLSearchParams
(
text
))
;
form
((
await
api
()
.
post
(
'
/login/device/code
'
)
.
type
(
'
form
'
)
.
send
(
{
client_id
:
GH
,
scope
:
'
repo
'
}
)
.
expect
(
200
))
.
text
)
;
(
'
/api/auth/device
'
)
.
query
(
{
user_code
:
code
.
user_code
}
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
)
;
(
'
/api/auth/device/approve
'
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
send
(
{
userCode
:
code
.
user_code
}
)
.
expect
(
200
)
;
form
((
await
api
()
.
post
(
'
/login/oauth/access_token
'
)
.
type
(
'
form
'
)
.
send
(
{
client_id
:
GH
,
device_code
:
code
.
device_code
,
grant_type
:
DEVICE_GRANT
}
))
.
text
)
;
(
'
/api/v3/user
'
)
.
set
(
'
authorization
'
,
`
token
${
token
.
access_token
}
`
)
.
expect
(
200
)
;
await
api
()
.
get
(
'
/api/oauth-apps/authorized
'
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
)
;
.
apps
)
.
toEqual
(
expect
.
arrayContaining
([
expect
.
objectContaining
(
{
clientId
:
GH
,
name
:
'
GitHub CLI
'
,
scopes
:
[
'
repo
'
]
}
)]))
;
(
`
/api/oauth-apps/authorized/
${
GH
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
204
)
;
(
'
/api/v3/user
'
)
.
set
(
'
authorization
'
,
`
token
${
token
.
access_token
}
`
)
.
expect
(
401
)
;
()
=>
{
()
=>
app
.
get
<
AuthService
<
Auth
>>
(
AuthService
)
.
api
;
`
oaorg
${
stamp
}
`
;
string
;
{
cookie
:
string
;
key
:
string
;
userId
:
string
}
;
{
cookie
:
string
;
key
:
string
;
userId
:
string
}
;
(
cookie
:
string
,
slug
=
org
)
=>
api
()
.
post
(
`
/api/organizations/
${
slug
}
/oauth-apps
`
)
.
set
(
'
cookie
'
,
cookie
)
.
send
(
{
name
:
'
Org Tool
'
,
homepageUrl
:
'
https://org.example
'
,
callbackUrls
:
[
'
https://org.example/callback
'
]
}
)
;
=>
{
signUp
(
app
,
`
oaadmin
${
stamp
}
`
)
;
signUp
(
app
,
`
oamember
${
stamp
}
`
)
;
auth
()
.
createOrganization
(
{
body
:
{
name
:
'
OAuth Org
'
,
slug
:
org
}
,
headers
:
new
Headers
(
{
cookie
:
owner
.
cookie
}
)
}
))
!
.
id
;
(
{
body
:
{
userId
:
admin
.
userId
,
role
:
'
admin
'
,
organizationId
}
}
)
;
(
{
body
:
{
userId
:
member
.
userId
,
role
:
'
member
'
,
organizationId
}
}
)
;
async
()
=>
{
}
=
await
createFor
(
owner
.
cookie
)
.
expect
(
201
)
;
)
.
toEqual
(
expect
.
any
(
String
))
;
=
await
db
.
select
(
{
userId
:
schema
.
oauthClient
.
userId
,
referenceId
:
schema
.
oauthClient
.
referenceId
}
)
.
from
(
schema
.
oauthClient
)
.
where
(
eq
(
schema
.
oauthClient
.
clientId
,
body
.
clientId
))
;
(
{
userId
:
null
,
referenceId
:
organizationId
}
)
;
await
api
()
.
get
(
`
/api/organizations/
${
org
}
/oauth-apps
`
)
.
set
(
'
cookie
'
,
admin
.
cookie
)
.
expect
(
200
)
;
.
apps
.
map
(
(
each
:
{
clientId
:
string
})
=>
each
.
clientId
))
.
toContain
(
body
.
clientId
)
;
await
api
()
.
get
(
'
/api/oauth-apps
'
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
200
)
;
.
apps
.
map
(
(
each
:
{
clientId
:
string
})
=>
each
.
clientId
))
.
not
.
toContain
(
body
.
clientId
)
;
await
api
()
.
get
(
'
/api/oauth-apps/authorize
'
)
.
query
(
{
client_id
:
body
.
clientId
}
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
expect
(
200
)
;
.
owner
)
.
toBe
(
org
)
;
async
()
=>
{
}
=
await
createFor
(
owner
.
cookie
)
.
expect
(
201
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
admin
.
cookie
)
.
expect
(
200
)
;
await
api
()
.
patch
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
admin
.
cookie
)
.
send
(
{
name
:
'
Renamed Org Tool
'
}
)
.
expect
(
200
)
;
.
name
)
.
toBe
(
'
Renamed Org Tool
'
)
;
await
api
()
.
post
(
`
/api/oauth-apps/
${
body
.
clientId
}
/secret
`
)
.
set
(
'
cookie
'
,
admin
.
cookie
)
.
expect
(
201
)
;
.
clientSecret
)
.
not
.
toBe
(
body
.
clientSecret
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
admin
.
cookie
)
.
expect
(
204
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
owner
.
cookie
)
.
expect
(
404
)
;
async
()
=>
{
}
=
await
createFor
(
owner
.
cookie
)
.
expect
(
201
)
;
.
cookie
)
.
expect
(
403
)
;
(
`
/api/organizations/
${
org
}
/oauth-apps
`
)
.
set
(
'
cookie
'
,
member
.
cookie
)
.
expect
(
403
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
member
.
cookie
)
.
send
(
{
name
:
'
Mine now
'
}
)
.
expect
(
403
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
/secret
`
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
expect
(
404
)
;
(
`
/api/oauth-apps/
${
body
.
clientId
}
`
)
.
set
(
'
cookie
'
,
other
.
cookie
)
.
expect
(
404
)
;
.
cookie
)
.
expect
(
404
)
;
async
()
=>
{
`
oagone
${
stamp
}
`
;
(
await
auth
()
.
createOrganization
(
{
body
:
{
name
:
'
Gone Org
'
,
slug
}
,
headers
:
new
Headers
(
{
cookie
:
owner
.
cookie
}
)
}
))
!
.
id
;
}
=
await
createFor
(
owner
.
cookie
,
slug
)
.
expect
(
201
)
;
(
schema
.
apikey
)
.
values
(
{
id
:
`
kgone
${
stamp
}
`
,
referenceId
:
other
.
userId
,
key
:
`
hashgone
${
stamp
}
`
,
name
:
'
k
'
,
metadata
:
JSON
.
stringify
(
{
oauthClientId
:
body
.
clientId
}
)
,
createdAt
:
new
Date
()
,
updatedAt
:
new
Date
()
}
)
;
(
{
body
:
{
organizationId
:
gone
}
,
headers
:
new
Headers
(
{
cookie
:
owner
.
cookie
}
)
}
)
;
.
select
()
.
from
(
schema
.
oauthClient
)
.
where
(
eq
(
schema
.
oauthClient
.
clientId
,
body
.
clientId
)))
.
toEqual
([])
;
.
select
()
.
from
(
schema
.
apikey
)
.
where
(
eq
(
schema
.
apikey
.
id
,
`
kgone
${
stamp
}
`
)))
.
toEqual
([])
;