diff options
-rw-r--r-- | config/default.yaml | 5 | ||||
-rw-r--r-- | config/production.yaml.example | 5 | ||||
-rw-r--r-- | server/initializers/checker-before-init.ts | 1 | ||||
-rw-r--r-- | server/initializers/config.ts | 6 | ||||
-rw-r--r-- | server/initializers/constants.ts | 6 | ||||
-rw-r--r-- | server/lib/auth/oauth.ts | 14 | ||||
-rw-r--r-- | server/lib/auth/tokens-cache.ts | 8 | ||||
-rw-r--r-- | server/tests/api/users/index.ts | 1 | ||||
-rw-r--r-- | server/tests/api/users/oauth.ts | 192 | ||||
-rw-r--r-- | server/tests/api/users/users.ts | 184 | ||||
-rw-r--r-- | shared/server-commands/requests/requests.ts | 2 |
11 files changed, 229 insertions, 195 deletions
diff --git a/config/default.yaml b/config/default.yaml index 1b7c3314d..d4977d003 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -37,6 +37,11 @@ rates_limit: | |||
37 | window: 10 minutes | 37 | window: 10 minutes |
38 | max: 10 | 38 | max: 10 |
39 | 39 | ||
40 | oauth2: | ||
41 | token_lifetime: | ||
42 | access_token: '1 day' | ||
43 | refresh_token: '2 weeks' | ||
44 | |||
40 | # Proxies to trust to get real client IP | 45 | # Proxies to trust to get real client IP |
41 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' | 46 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' |
42 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) | 47 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) |
diff --git a/config/production.yaml.example b/config/production.yaml.example index da067b3b5..17dc6839b 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -35,6 +35,11 @@ rates_limit: | |||
35 | window: 10 minutes | 35 | window: 10 minutes |
36 | max: 10 | 36 | max: 10 |
37 | 37 | ||
38 | oauth2: | ||
39 | token_lifetime: | ||
40 | access_token: '1 day' | ||
41 | refresh_token: '2 weeks' | ||
42 | |||
38 | # Proxies to trust to get real client IP | 43 | # Proxies to trust to get real client IP |
39 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' | 44 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' |
40 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) | 45 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) |
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 39713a266..57852241c 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -13,6 +13,7 @@ function checkMissedConfig () { | |||
13 | 'webserver.https', 'webserver.hostname', 'webserver.port', | 13 | 'webserver.https', 'webserver.hostname', 'webserver.port', |
14 | 'secrets.peertube', | 14 | 'secrets.peertube', |
15 | 'trust_proxy', | 15 | 'trust_proxy', |
16 | 'oauth2.token_lifetime.access_token', 'oauth2.token_lifetime.refresh_token', | ||
16 | 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max', | 17 | 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max', |
17 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', | 18 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', |
18 | 'email.body.signature', 'email.subject.prefix', | 19 | 'email.body.signature', 'email.subject.prefix', |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index c2f8b19fd..28aaf36a9 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -149,6 +149,12 @@ const CONFIG = { | |||
149 | HOSTNAME: config.get<string>('webserver.hostname'), | 149 | HOSTNAME: config.get<string>('webserver.hostname'), |
150 | PORT: config.get<number>('webserver.port') | 150 | PORT: config.get<number>('webserver.port') |
151 | }, | 151 | }, |
152 | OAUTH2: { | ||
153 | TOKEN_LIFETIME: { | ||
154 | ACCESS_TOKEN: parseDurationToMs(config.get<string>('oauth2.token_lifetime.access_token')), | ||
155 | REFRESH_TOKEN: parseDurationToMs(config.get<string>('oauth2.token_lifetime.refresh_token')) | ||
156 | } | ||
157 | }, | ||
152 | RATES_LIMIT: { | 158 | RATES_LIMIT: { |
153 | API: { | 159 | API: { |
154 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')), | 160 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')), |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index ec5045078..0dab524d9 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -101,11 +101,6 @@ const SORTABLE_COLUMNS = { | |||
101 | VIDEO_REDUNDANCIES: [ 'name' ] | 101 | VIDEO_REDUNDANCIES: [ 'name' ] |
102 | } | 102 | } |
103 | 103 | ||
104 | const OAUTH_LIFETIME = { | ||
105 | ACCESS_TOKEN: 3600 * 24, // 1 day, for upload | ||
106 | REFRESH_TOKEN: 1209600 // 2 weeks | ||
107 | } | ||
108 | |||
109 | const ROUTE_CACHE_LIFETIME = { | 104 | const ROUTE_CACHE_LIFETIME = { |
110 | FEEDS: '15 minutes', | 105 | FEEDS: '15 minutes', |
111 | ROBOTS: '2 hours', | 106 | ROBOTS: '2 hours', |
@@ -1033,7 +1028,6 @@ export { | |||
1033 | JOB_ATTEMPTS, | 1028 | JOB_ATTEMPTS, |
1034 | AP_CLEANER, | 1029 | AP_CLEANER, |
1035 | LAST_MIGRATION_VERSION, | 1030 | LAST_MIGRATION_VERSION, |
1036 | OAUTH_LIFETIME, | ||
1037 | CUSTOM_HTML_TAG_COMMENTS, | 1031 | CUSTOM_HTML_TAG_COMMENTS, |
1038 | STATS_TIMESERIE, | 1032 | STATS_TIMESERIE, |
1039 | BROADCAST_CONCURRENCY, | 1033 | BROADCAST_CONCURRENCY, |
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts index bc0d4301f..2905c79a2 100644 --- a/server/lib/auth/oauth.ts +++ b/server/lib/auth/oauth.ts | |||
@@ -10,10 +10,11 @@ import OAuth2Server, { | |||
10 | } from '@node-oauth/oauth2-server' | 10 | } from '@node-oauth/oauth2-server' |
11 | import { randomBytesPromise } from '@server/helpers/core-utils' | 11 | import { randomBytesPromise } from '@server/helpers/core-utils' |
12 | import { isOTPValid } from '@server/helpers/otp' | 12 | import { isOTPValid } from '@server/helpers/otp' |
13 | import { CONFIG } from '@server/initializers/config' | ||
13 | import { MOAuthClient } from '@server/types/models' | 14 | import { MOAuthClient } from '@server/types/models' |
14 | import { sha1 } from '@shared/extra-utils' | 15 | import { sha1 } from '@shared/extra-utils' |
15 | import { HttpStatusCode } from '@shared/models' | 16 | import { HttpStatusCode } from '@shared/models' |
16 | import { OAUTH_LIFETIME, OTP } from '../../initializers/constants' | 17 | import { OTP } from '../../initializers/constants' |
17 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' | 18 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' |
18 | 19 | ||
19 | class MissingTwoFactorError extends Error { | 20 | class MissingTwoFactorError extends Error { |
@@ -32,8 +33,9 @@ class InvalidTwoFactorError extends Error { | |||
32 | * | 33 | * |
33 | */ | 34 | */ |
34 | const oAuthServer = new OAuth2Server({ | 35 | const oAuthServer = new OAuth2Server({ |
35 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | 36 | // Wants seconds |
36 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | 37 | accessTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN / 1000, |
38 | refreshTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN / 1000, | ||
37 | 39 | ||
38 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | 40 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications |
39 | model: require('./oauth-model') | 41 | model: require('./oauth-model') |
@@ -182,10 +184,10 @@ function generateRandomToken () { | |||
182 | 184 | ||
183 | function getTokenExpiresAt (type: 'access' | 'refresh') { | 185 | function getTokenExpiresAt (type: 'access' | 'refresh') { |
184 | const lifetime = type === 'access' | 186 | const lifetime = type === 'access' |
185 | ? OAUTH_LIFETIME.ACCESS_TOKEN | 187 | ? CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN |
186 | : OAUTH_LIFETIME.REFRESH_TOKEN | 188 | : CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN |
187 | 189 | ||
188 | return new Date(Date.now() + lifetime * 1000) | 190 | return new Date(Date.now() + lifetime) |
189 | } | 191 | } |
190 | 192 | ||
191 | async function buildToken () { | 193 | async function buildToken () { |
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts index 410708a35..43efc7d02 100644 --- a/server/lib/auth/tokens-cache.ts +++ b/server/lib/auth/tokens-cache.ts | |||
@@ -36,8 +36,8 @@ export class TokensCache { | |||
36 | const token = this.userHavingToken.get(userId) | 36 | const token = this.userHavingToken.get(userId) |
37 | 37 | ||
38 | if (token !== undefined) { | 38 | if (token !== undefined) { |
39 | this.accessTokenCache.del(token) | 39 | this.accessTokenCache.delete(token) |
40 | this.userHavingToken.del(userId) | 40 | this.userHavingToken.delete(userId) |
41 | } | 41 | } |
42 | } | 42 | } |
43 | 43 | ||
@@ -45,8 +45,8 @@ export class TokensCache { | |||
45 | const tokenModel = this.accessTokenCache.get(token) | 45 | const tokenModel = this.accessTokenCache.get(token) |
46 | 46 | ||
47 | if (tokenModel !== undefined) { | 47 | if (tokenModel !== undefined) { |
48 | this.userHavingToken.del(tokenModel.userId) | 48 | this.userHavingToken.delete(tokenModel.userId) |
49 | this.accessTokenCache.del(token) | 49 | this.accessTokenCache.delete(token) |
50 | } | 50 | } |
51 | } | 51 | } |
52 | } | 52 | } |
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts index 643f1a531..0313845ef 100644 --- a/server/tests/api/users/index.ts +++ b/server/tests/api/users/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import './oauth' | ||
1 | import './two-factor' | 2 | import './two-factor' |
2 | import './user-subscriptions' | 3 | import './user-subscriptions' |
3 | import './user-videos' | 4 | import './user-videos' |
diff --git a/server/tests/api/users/oauth.ts b/server/tests/api/users/oauth.ts new file mode 100644 index 000000000..6a3da5ea2 --- /dev/null +++ b/server/tests/api/users/oauth.ts | |||
@@ -0,0 +1,192 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@shared/core-utils' | ||
5 | import { HttpStatusCode, OAuth2ErrorCode, PeerTubeProblemDocument } from '@shared/models' | ||
6 | import { cleanupTests, createSingleServer, killallServers, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
7 | |||
8 | describe('Test oauth', function () { | ||
9 | let server: PeerTubeServer | ||
10 | |||
11 | before(async function () { | ||
12 | this.timeout(30000) | ||
13 | |||
14 | server = await createSingleServer(1, { | ||
15 | rates_limit: { | ||
16 | login: { | ||
17 | max: 30 | ||
18 | } | ||
19 | } | ||
20 | }) | ||
21 | |||
22 | await setAccessTokensToServers([ server ]) | ||
23 | }) | ||
24 | |||
25 | describe('OAuth client', function () { | ||
26 | |||
27 | function expectInvalidClient (body: PeerTubeProblemDocument) { | ||
28 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) | ||
29 | expect(body.error).to.contain('client is invalid') | ||
30 | expect(body.type.startsWith('https://')).to.be.true | ||
31 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
32 | } | ||
33 | |||
34 | it('Should create a new client') | ||
35 | |||
36 | it('Should return the first client') | ||
37 | |||
38 | it('Should remove the last client') | ||
39 | |||
40 | it('Should not login with an invalid client id', async function () { | ||
41 | const client = { id: 'client', secret: server.store.client.secret } | ||
42 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
43 | |||
44 | expectInvalidClient(body) | ||
45 | }) | ||
46 | |||
47 | it('Should not login with an invalid client secret', async function () { | ||
48 | const client = { id: server.store.client.id, secret: 'coucou' } | ||
49 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
50 | |||
51 | expectInvalidClient(body) | ||
52 | }) | ||
53 | }) | ||
54 | |||
55 | describe('Login', function () { | ||
56 | |||
57 | function expectInvalidCredentials (body: PeerTubeProblemDocument) { | ||
58 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) | ||
59 | expect(body.error).to.contain('credentials are invalid') | ||
60 | expect(body.type.startsWith('https://')).to.be.true | ||
61 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
62 | } | ||
63 | |||
64 | it('Should not login with an invalid username', async function () { | ||
65 | const user = { username: 'captain crochet', password: server.store.user.password } | ||
66 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
67 | |||
68 | expectInvalidCredentials(body) | ||
69 | }) | ||
70 | |||
71 | it('Should not login with an invalid password', async function () { | ||
72 | const user = { username: server.store.user.username, password: 'mew_three' } | ||
73 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
74 | |||
75 | expectInvalidCredentials(body) | ||
76 | }) | ||
77 | |||
78 | it('Should be able to login', async function () { | ||
79 | await server.login.login({ expectedStatus: HttpStatusCode.OK_200 }) | ||
80 | }) | ||
81 | |||
82 | it('Should be able to login with an insensitive username', async function () { | ||
83 | const user = { username: 'RoOt', password: server.store.user.password } | ||
84 | await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 }) | ||
85 | |||
86 | const user2 = { username: 'rOoT', password: server.store.user.password } | ||
87 | await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 }) | ||
88 | |||
89 | const user3 = { username: 'ROOt', password: server.store.user.password } | ||
90 | await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 }) | ||
91 | }) | ||
92 | }) | ||
93 | |||
94 | describe('Logout', function () { | ||
95 | |||
96 | it('Should logout (revoke token)', async function () { | ||
97 | await server.login.logout({ token: server.accessToken }) | ||
98 | }) | ||
99 | |||
100 | it('Should not be able to get the user information', async function () { | ||
101 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
102 | }) | ||
103 | |||
104 | it('Should not be able to upload a video', async function () { | ||
105 | await server.videos.upload({ attributes: { name: 'video' }, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
106 | }) | ||
107 | |||
108 | it('Should be able to login again', async function () { | ||
109 | const body = await server.login.login() | ||
110 | server.accessToken = body.access_token | ||
111 | server.refreshToken = body.refresh_token | ||
112 | }) | ||
113 | |||
114 | it('Should be able to get my user information again', async function () { | ||
115 | await server.users.getMyInfo() | ||
116 | }) | ||
117 | |||
118 | it('Should have an expired access token', async function () { | ||
119 | this.timeout(60000) | ||
120 | |||
121 | await server.sql.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString()) | ||
122 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString()) | ||
123 | |||
124 | await killallServers([ server ]) | ||
125 | await server.run() | ||
126 | |||
127 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
128 | }) | ||
129 | |||
130 | it('Should not be able to refresh an access token with an expired refresh token', async function () { | ||
131 | await server.login.refreshToken({ refreshToken: server.refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
132 | }) | ||
133 | |||
134 | it('Should refresh the token', async function () { | ||
135 | this.timeout(50000) | ||
136 | |||
137 | const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString() | ||
138 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate) | ||
139 | |||
140 | await killallServers([ server ]) | ||
141 | await server.run() | ||
142 | |||
143 | const res = await server.login.refreshToken({ refreshToken: server.refreshToken }) | ||
144 | server.accessToken = res.body.access_token | ||
145 | server.refreshToken = res.body.refresh_token | ||
146 | }) | ||
147 | |||
148 | it('Should be able to get my user information again', async function () { | ||
149 | await server.users.getMyInfo() | ||
150 | }) | ||
151 | }) | ||
152 | |||
153 | describe('Custom token lifetime', function () { | ||
154 | before(async function () { | ||
155 | this.timeout(120_000) | ||
156 | |||
157 | await server.kill() | ||
158 | await server.run({ | ||
159 | oauth2: { | ||
160 | token_lifetime: { | ||
161 | access_token: '2 seconds', | ||
162 | refresh_token: '2 seconds' | ||
163 | } | ||
164 | } | ||
165 | }) | ||
166 | }) | ||
167 | |||
168 | it('Should have a very short access token lifetime', async function () { | ||
169 | this.timeout(50000) | ||
170 | |||
171 | const { access_token: accessToken } = await server.login.login() | ||
172 | await server.users.getMyInfo({ token: accessToken }) | ||
173 | |||
174 | await wait(3000) | ||
175 | await server.users.getMyInfo({ token: accessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
176 | }) | ||
177 | |||
178 | it('Should have a very short refresh token lifetime', async function () { | ||
179 | this.timeout(50000) | ||
180 | |||
181 | const { refresh_token: refreshToken } = await server.login.login() | ||
182 | await server.login.refreshToken({ refreshToken }) | ||
183 | |||
184 | await wait(3000) | ||
185 | await server.login.refreshToken({ refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
186 | }) | ||
187 | }) | ||
188 | |||
189 | after(async function () { | ||
190 | await cleanupTests([ server ]) | ||
191 | }) | ||
192 | }) | ||
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 421b3ce16..93e2e489a 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -2,15 +2,8 @@ | |||
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { testImage } from '@server/tests/shared' | 4 | import { testImage } from '@server/tests/shared' |
5 | import { AbuseState, HttpStatusCode, OAuth2ErrorCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@shared/models' | 5 | import { AbuseState, HttpStatusCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@shared/models' |
6 | import { | 6 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' |
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | killallServers, | ||
10 | makePutBodyRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@shared/server-commands' | ||
14 | 7 | ||
15 | describe('Test users', function () { | 8 | describe('Test users', function () { |
16 | let server: PeerTubeServer | 9 | let server: PeerTubeServer |
@@ -39,166 +32,6 @@ describe('Test users', function () { | |||
39 | await server.plugins.install({ npmName: 'peertube-theme-background-red' }) | 32 | await server.plugins.install({ npmName: 'peertube-theme-background-red' }) |
40 | }) | 33 | }) |
41 | 34 | ||
42 | describe('OAuth client', function () { | ||
43 | it('Should create a new client') | ||
44 | |||
45 | it('Should return the first client') | ||
46 | |||
47 | it('Should remove the last client') | ||
48 | |||
49 | it('Should not login with an invalid client id', async function () { | ||
50 | const client = { id: 'client', secret: server.store.client.secret } | ||
51 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
52 | |||
53 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) | ||
54 | expect(body.error).to.contain('client is invalid') | ||
55 | expect(body.type.startsWith('https://')).to.be.true | ||
56 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
57 | }) | ||
58 | |||
59 | it('Should not login with an invalid client secret', async function () { | ||
60 | const client = { id: server.store.client.id, secret: 'coucou' } | ||
61 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
62 | |||
63 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) | ||
64 | expect(body.error).to.contain('client is invalid') | ||
65 | expect(body.type.startsWith('https://')).to.be.true | ||
66 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
67 | }) | ||
68 | }) | ||
69 | |||
70 | describe('Login', function () { | ||
71 | |||
72 | it('Should not login with an invalid username', async function () { | ||
73 | const user = { username: 'captain crochet', password: server.store.user.password } | ||
74 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
75 | |||
76 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) | ||
77 | expect(body.error).to.contain('credentials are invalid') | ||
78 | expect(body.type.startsWith('https://')).to.be.true | ||
79 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
80 | }) | ||
81 | |||
82 | it('Should not login with an invalid password', async function () { | ||
83 | const user = { username: server.store.user.username, password: 'mew_three' } | ||
84 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
85 | |||
86 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) | ||
87 | expect(body.error).to.contain('credentials are invalid') | ||
88 | expect(body.type.startsWith('https://')).to.be.true | ||
89 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
90 | }) | ||
91 | |||
92 | it('Should not be able to upload a video', async function () { | ||
93 | token = 'my_super_token' | ||
94 | |||
95 | await server.videos.upload({ token, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
96 | }) | ||
97 | |||
98 | it('Should not be able to follow', async function () { | ||
99 | token = 'my_super_token' | ||
100 | |||
101 | await server.follows.follow({ | ||
102 | hosts: [ 'http://example.com' ], | ||
103 | token, | ||
104 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
105 | }) | ||
106 | }) | ||
107 | |||
108 | it('Should not be able to unfollow') | ||
109 | |||
110 | it('Should be able to login', async function () { | ||
111 | const body = await server.login.login({ expectedStatus: HttpStatusCode.OK_200 }) | ||
112 | |||
113 | token = body.access_token | ||
114 | }) | ||
115 | |||
116 | it('Should be able to login with an insensitive username', async function () { | ||
117 | const user = { username: 'RoOt', password: server.store.user.password } | ||
118 | await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 }) | ||
119 | |||
120 | const user2 = { username: 'rOoT', password: server.store.user.password } | ||
121 | await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 }) | ||
122 | |||
123 | const user3 = { username: 'ROOt', password: server.store.user.password } | ||
124 | await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 }) | ||
125 | }) | ||
126 | }) | ||
127 | |||
128 | describe('Logout', function () { | ||
129 | it('Should logout (revoke token)', async function () { | ||
130 | await server.login.logout({ token: server.accessToken }) | ||
131 | }) | ||
132 | |||
133 | it('Should not be able to get the user information', async function () { | ||
134 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
135 | }) | ||
136 | |||
137 | it('Should not be able to upload a video', async function () { | ||
138 | await server.videos.upload({ attributes: { name: 'video' }, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
139 | }) | ||
140 | |||
141 | it('Should not be able to rate a video', async function () { | ||
142 | const path = '/api/v1/videos/' | ||
143 | const data = { | ||
144 | rating: 'likes' | ||
145 | } | ||
146 | |||
147 | const options = { | ||
148 | url: server.url, | ||
149 | path: path + videoId, | ||
150 | token: 'wrong token', | ||
151 | fields: data, | ||
152 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
153 | } | ||
154 | await makePutBodyRequest(options) | ||
155 | }) | ||
156 | |||
157 | it('Should be able to login again', async function () { | ||
158 | const body = await server.login.login() | ||
159 | server.accessToken = body.access_token | ||
160 | server.refreshToken = body.refresh_token | ||
161 | }) | ||
162 | |||
163 | it('Should be able to get my user information again', async function () { | ||
164 | await server.users.getMyInfo() | ||
165 | }) | ||
166 | |||
167 | it('Should have an expired access token', async function () { | ||
168 | this.timeout(60000) | ||
169 | |||
170 | await server.sql.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString()) | ||
171 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString()) | ||
172 | |||
173 | await killallServers([ server ]) | ||
174 | await server.run() | ||
175 | |||
176 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
177 | }) | ||
178 | |||
179 | it('Should not be able to refresh an access token with an expired refresh token', async function () { | ||
180 | await server.login.refreshToken({ refreshToken: server.refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
181 | }) | ||
182 | |||
183 | it('Should refresh the token', async function () { | ||
184 | this.timeout(50000) | ||
185 | |||
186 | const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString() | ||
187 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate) | ||
188 | |||
189 | await killallServers([ server ]) | ||
190 | await server.run() | ||
191 | |||
192 | const res = await server.login.refreshToken({ refreshToken: server.refreshToken }) | ||
193 | server.accessToken = res.body.access_token | ||
194 | server.refreshToken = res.body.refresh_token | ||
195 | }) | ||
196 | |||
197 | it('Should be able to get my user information again', async function () { | ||
198 | await server.users.getMyInfo() | ||
199 | }) | ||
200 | }) | ||
201 | |||
202 | describe('Creating a user', function () { | 35 | describe('Creating a user', function () { |
203 | 36 | ||
204 | it('Should be able to create a new user', async function () { | 37 | it('Should be able to create a new user', async function () { |
@@ -512,6 +345,7 @@ describe('Test users', function () { | |||
512 | }) | 345 | }) |
513 | 346 | ||
514 | describe('Updating another user', function () { | 347 | describe('Updating another user', function () { |
348 | |||
515 | it('Should be able to update another user', async function () { | 349 | it('Should be able to update another user', async function () { |
516 | await server.users.update({ | 350 | await server.users.update({ |
517 | userId, | 351 | userId, |
@@ -562,13 +396,6 @@ describe('Test users', function () { | |||
562 | }) | 396 | }) |
563 | }) | 397 | }) |
564 | 398 | ||
565 | describe('Video blacklists', function () { | ||
566 | |||
567 | it('Should be able to list my video blacklist', async function () { | ||
568 | await server.blacklist.list({ token: userToken }) | ||
569 | }) | ||
570 | }) | ||
571 | |||
572 | describe('Remove a user', function () { | 399 | describe('Remove a user', function () { |
573 | 400 | ||
574 | before(async function () { | 401 | before(async function () { |
@@ -653,8 +480,9 @@ describe('Test users', function () { | |||
653 | }) | 480 | }) |
654 | 481 | ||
655 | describe('User blocking', function () { | 482 | describe('User blocking', function () { |
656 | let user16Id | 483 | let user16Id: number |
657 | let user16AccessToken | 484 | let user16AccessToken: string |
485 | |||
658 | const user16 = { | 486 | const user16 = { |
659 | username: 'user_16', | 487 | username: 'user_16', |
660 | password: 'my super password' | 488 | password: 'my super password' |
diff --git a/shared/server-commands/requests/requests.ts b/shared/server-commands/requests/requests.ts index dc9cf4e01..cb0e1a5fb 100644 --- a/shared/server-commands/requests/requests.ts +++ b/shared/server-commands/requests/requests.ts | |||
@@ -199,7 +199,7 @@ function buildRequest (req: request.Test, options: CommonRequestParams) { | |||
199 | return req.expect((res) => { | 199 | return req.expect((res) => { |
200 | if (options.expectedStatus && res.status !== options.expectedStatus) { | 200 | if (options.expectedStatus && res.status !== options.expectedStatus) { |
201 | throw new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` + | 201 | throw new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` + |
202 | `\nThe server responded this error: "${res.body?.error ?? res.text}".\n` + | 202 | `\nThe server responded: "${res.body?.error ?? res.text}".\n` + |
203 | 'You may take a closer look at the logs. To see how to do so, check out this page: ' + | 203 | 'You may take a closer look at the logs. To see how to do so, check out this page: ' + |
204 | 'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs') | 204 | 'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs') |
205 | } | 205 | } |