1
+ import { Request , Response } from "express" ;
2
+ import { requireBearerAuth } from "./bearerAuth.js" ;
3
+ import { AuthInfo } from "../types.js" ;
4
+ import { InsufficientScopeError , InvalidTokenError , OAuthError , ServerError } from "../errors.js" ;
5
+ import { OAuthServerProvider } from "../provider.js" ;
6
+ import { OAuthRegisteredClientsStore } from "../clients.js" ;
7
+
8
+ // Mock provider
9
+ const mockVerifyAccessToken = jest . fn ( ) ;
10
+ const mockProvider : OAuthServerProvider = {
11
+ clientsStore : { } as OAuthRegisteredClientsStore ,
12
+ authorize : jest . fn ( ) ,
13
+ challengeForAuthorizationCode : jest . fn ( ) ,
14
+ exchangeAuthorizationCode : jest . fn ( ) ,
15
+ exchangeRefreshToken : jest . fn ( ) ,
16
+ verifyAccessToken : mockVerifyAccessToken ,
17
+ } ;
18
+
19
+ describe ( "requireBearerAuth middleware" , ( ) => {
20
+ let mockRequest : Partial < Request > ;
21
+ let mockResponse : Partial < Response > ;
22
+ let nextFunction : jest . Mock ;
23
+
24
+ beforeEach ( ( ) => {
25
+ mockRequest = {
26
+ headers : { } ,
27
+ } ;
28
+ mockResponse = {
29
+ status : jest . fn ( ) . mockReturnThis ( ) ,
30
+ json : jest . fn ( ) ,
31
+ set : jest . fn ( ) . mockReturnThis ( ) ,
32
+ } ;
33
+ nextFunction = jest . fn ( ) ;
34
+ jest . clearAllMocks ( ) ;
35
+ } ) ;
36
+
37
+ it ( "should call next when token is valid" , async ( ) => {
38
+ const validAuthInfo : AuthInfo = {
39
+ token : "valid-token" ,
40
+ clientId : "client-123" ,
41
+ scopes : [ "read" , "write" ] ,
42
+ } ;
43
+ mockVerifyAccessToken . mockResolvedValue ( validAuthInfo ) ;
44
+
45
+ mockRequest . headers = {
46
+ authorization : "Bearer valid-token" ,
47
+ } ;
48
+
49
+ const middleware = requireBearerAuth ( { provider : mockProvider } ) ;
50
+ await middleware ( mockRequest as Request , mockResponse as Response , nextFunction ) ;
51
+
52
+ expect ( mockVerifyAccessToken ) . toHaveBeenCalledWith ( "valid-token" ) ;
53
+ expect ( mockRequest . auth ) . toEqual ( validAuthInfo ) ;
54
+ expect ( nextFunction ) . toHaveBeenCalled ( ) ;
55
+ expect ( mockResponse . status ) . not . toHaveBeenCalled ( ) ;
56
+ expect ( mockResponse . json ) . not . toHaveBeenCalled ( ) ;
57
+ } ) ;
58
+
59
+ it ( "should require specific scopes when configured" , async ( ) => {
60
+ const authInfo : AuthInfo = {
61
+ token : "valid-token" ,
62
+ clientId : "client-123" ,
63
+ scopes : [ "read" ] ,
64
+ } ;
65
+ mockVerifyAccessToken . mockResolvedValue ( authInfo ) ;
66
+
67
+ mockRequest . headers = {
68
+ authorization : "Bearer valid-token" ,
69
+ } ;
70
+
71
+ const middleware = requireBearerAuth ( {
72
+ provider : mockProvider ,
73
+ requiredScopes : [ "read" , "write" ]
74
+ } ) ;
75
+
76
+ await middleware ( mockRequest as Request , mockResponse as Response , nextFunction ) ;
77
+
78
+ expect ( mockVerifyAccessToken ) . toHaveBeenCalledWith ( "valid-token" ) ;
79
+ expect ( mockResponse . status ) . toHaveBeenCalledWith ( 403 ) ;
80
+ expect ( mockResponse . set ) . toHaveBeenCalledWith (
81
+ "WWW-Authenticate" ,
82
+ expect . stringContaining ( 'Bearer error="insufficient_scope"' )
83
+ ) ;
84
+ expect ( mockResponse . json ) . toHaveBeenCalledWith (
85
+ expect . objectContaining ( { error : "insufficient_scope" , error_description : "Insufficient scope" } )
86
+ ) ;
87
+ expect ( nextFunction ) . not . toHaveBeenCalled ( ) ;
88
+ } ) ;
89
+
90
+ it ( "should accept token with all required scopes" , async ( ) => {
91
+ const authInfo : AuthInfo = {
92
+ token : "valid-token" ,
93
+ clientId : "client-123" ,
94
+ scopes : [ "read" , "write" , "admin" ] ,
95
+ } ;
96
+ mockVerifyAccessToken . mockResolvedValue ( authInfo ) ;
97
+
98
+ mockRequest . headers = {
99
+ authorization : "Bearer valid-token" ,
100
+ } ;
101
+
102
+ const middleware = requireBearerAuth ( {
103
+ provider : mockProvider ,
104
+ requiredScopes : [ "read" , "write" ]
105
+ } ) ;
106
+
107
+ await middleware ( mockRequest as Request , mockResponse as Response , nextFunction ) ;
108
+
109
+ expect ( mockVerifyAccessToken ) . toHaveBeenCalledWith ( "valid-token" ) ;
110
+ expect ( mockRequest . auth ) . toEqual ( authInfo ) ;
111
+ expect ( nextFunction ) . toHaveBeenCalled ( ) ;
112
+ expect ( mockResponse . status ) . not . toHaveBeenCalled ( ) ;
113
+ expect ( mockResponse . json ) . not . toHaveBeenCalled ( ) ;
114
+ } ) ;
115
+
116
+ it ( "should return 401 when no Authorization header is present" , async ( ) => {
117
+ const middleware = requireBearerAuth ( { provider : mockProvider } ) ;
118
+ await middleware ( mockRequest as Request , mockResponse as Response , nextFunction ) ;
119
+
120
+ expect ( mockVerifyAccessToken ) . not . toHaveBeenCalled ( ) ;
121
+ expect ( mockResponse . status ) . toHaveBeenCalledWith ( 401 ) ;
122
+ expect ( mockResponse . set ) . toHaveBeenCalledWith (
123
+ "WWW-Authenticate" ,
124
+ expect . stringContaining ( 'Bearer error="invalid_token"' )
125
+ ) ;
126
+ expect ( mockResponse . json ) . toHaveBeenCalledWith (
127
+ expect . objectContaining ( { error : "invalid_token" , error_description : "Missing Authorization header" } )
128
+ ) ;
129
+ expect ( nextFunction ) . not . toHaveBeenCalled ( ) ;
130
+ } ) ;
131
+
132
+ it ( "should return 401 when Authorization header format is invalid" , async ( ) => {
133
+ mockRequest . headers = {
134
+ authorization : "InvalidFormat" ,
135
+ } ;
136
+
137
+ const middleware = requireBearerAuth ( { provider : mockProvider } ) ;
138
+ await middleware ( mockRequest as Request , mockResponse as Response , nextFunction ) ;
139
+
140
+ expect ( mockVerifyAccessToken ) . not . toHaveBeenCalled ( ) ;
141
+ expect ( mockResponse . status ) . toHaveBeenCalledWith ( 401 ) ;
142
+ expect ( mockResponse . set ) . toHaveBeenCalledWith (
143
+ "WWW-Authenticate" ,
144
+ expect . stringContaining ( 'Bearer error="invalid_token"' )
145
+ ) ;
146
+ expect ( mockResponse . json ) . toHaveBeenCalledWith (
147
+ expect . objectContaining ( {
148
+ error : "invalid_token" ,
149
+ error_description : "Invalid Authorization header format, expected 'Bearer TOKEN'"
150
+ } )
151
+ ) ;
152
+ expect ( nextFunction ) . not . toHaveBeenCalled ( ) ;
153
+ } ) ;
154
+
155
+ it ( "should return 401 when token verification fails with InvalidTokenError" , async ( ) => {
156
+ mockRequest . headers = {
157
+ authorization : "Bearer invalid-token" ,
158
+ } ;
159
+
160
+ mockVerifyAccessToken . mockRejectedValue ( new InvalidTokenError ( "Token expired" ) ) ;
161
+
162
+ const middleware = requireBearerAuth ( { provider : mockProvider } ) ;
163
+ await middleware ( mockRequest as Request , mockResponse as Response , nextFunction ) ;
164
+
165
+ expect ( mockVerifyAccessToken ) . toHaveBeenCalledWith ( "invalid-token" ) ;
166
+ expect ( mockResponse . status ) . toHaveBeenCalledWith ( 401 ) ;
167
+ expect ( mockResponse . set ) . toHaveBeenCalledWith (
168
+ "WWW-Authenticate" ,
169
+ expect . stringContaining ( 'Bearer error="invalid_token"' )
170
+ ) ;
171
+ expect ( mockResponse . json ) . toHaveBeenCalledWith (
172
+ expect . objectContaining ( { error : "invalid_token" , error_description : "Token expired" } )
173
+ ) ;
174
+ expect ( nextFunction ) . not . toHaveBeenCalled ( ) ;
175
+ } ) ;
176
+
177
+ it ( "should return 403 when access token has insufficient scopes" , async ( ) => {
178
+ mockRequest . headers = {
179
+ authorization : "Bearer valid-token" ,
180
+ } ;
181
+
182
+ mockVerifyAccessToken . mockRejectedValue ( new InsufficientScopeError ( "Required scopes: read, write" ) ) ;
183
+
184
+ const middleware = requireBearerAuth ( { provider : mockProvider } ) ;
185
+ await middleware ( mockRequest as Request , mockResponse as Response , nextFunction ) ;
186
+
187
+ expect ( mockVerifyAccessToken ) . toHaveBeenCalledWith ( "valid-token" ) ;
188
+ expect ( mockResponse . status ) . toHaveBeenCalledWith ( 403 ) ;
189
+ expect ( mockResponse . set ) . toHaveBeenCalledWith (
190
+ "WWW-Authenticate" ,
191
+ expect . stringContaining ( 'Bearer error="insufficient_scope"' )
192
+ ) ;
193
+ expect ( mockResponse . json ) . toHaveBeenCalledWith (
194
+ expect . objectContaining ( { error : "insufficient_scope" , error_description : "Required scopes: read, write" } )
195
+ ) ;
196
+ expect ( nextFunction ) . not . toHaveBeenCalled ( ) ;
197
+ } ) ;
198
+
199
+ it ( "should return 500 when a ServerError occurs" , async ( ) => {
200
+ mockRequest . headers = {
201
+ authorization : "Bearer valid-token" ,
202
+ } ;
203
+
204
+ mockVerifyAccessToken . mockRejectedValue ( new ServerError ( "Internal server issue" ) ) ;
205
+
206
+ const middleware = requireBearerAuth ( { provider : mockProvider } ) ;
207
+ await middleware ( mockRequest as Request , mockResponse as Response , nextFunction ) ;
208
+
209
+ expect ( mockVerifyAccessToken ) . toHaveBeenCalledWith ( "valid-token" ) ;
210
+ expect ( mockResponse . status ) . toHaveBeenCalledWith ( 500 ) ;
211
+ expect ( mockResponse . json ) . toHaveBeenCalledWith (
212
+ expect . objectContaining ( { error : "server_error" , error_description : "Internal server issue" } )
213
+ ) ;
214
+ expect ( nextFunction ) . not . toHaveBeenCalled ( ) ;
215
+ } ) ;
216
+
217
+ it ( "should return 400 for generic OAuthError" , async ( ) => {
218
+ mockRequest . headers = {
219
+ authorization : "Bearer valid-token" ,
220
+ } ;
221
+
222
+ mockVerifyAccessToken . mockRejectedValue ( new OAuthError ( "custom_error" , "Some OAuth error" ) ) ;
223
+
224
+ const middleware = requireBearerAuth ( { provider : mockProvider } ) ;
225
+ await middleware ( mockRequest as Request , mockResponse as Response , nextFunction ) ;
226
+
227
+ expect ( mockVerifyAccessToken ) . toHaveBeenCalledWith ( "valid-token" ) ;
228
+ expect ( mockResponse . status ) . toHaveBeenCalledWith ( 400 ) ;
229
+ expect ( mockResponse . json ) . toHaveBeenCalledWith (
230
+ expect . objectContaining ( { error : "custom_error" , error_description : "Some OAuth error" } )
231
+ ) ;
232
+ expect ( nextFunction ) . not . toHaveBeenCalled ( ) ;
233
+ } ) ;
234
+
235
+ it ( "should return 500 when unexpected error occurs" , async ( ) => {
236
+ mockRequest . headers = {
237
+ authorization : "Bearer valid-token" ,
238
+ } ;
239
+
240
+ mockVerifyAccessToken . mockRejectedValue ( new Error ( "Unexpected error" ) ) ;
241
+
242
+ const middleware = requireBearerAuth ( { provider : mockProvider } ) ;
243
+ await middleware ( mockRequest as Request , mockResponse as Response , nextFunction ) ;
244
+
245
+ expect ( mockVerifyAccessToken ) . toHaveBeenCalledWith ( "valid-token" ) ;
246
+ expect ( mockResponse . status ) . toHaveBeenCalledWith ( 500 ) ;
247
+ expect ( mockResponse . json ) . toHaveBeenCalledWith (
248
+ expect . objectContaining ( { error : "server_error" , error_description : "Internal Server Error" } )
249
+ ) ;
250
+ expect ( nextFunction ) . not . toHaveBeenCalled ( ) ;
251
+ } ) ;
252
+ } ) ;
0 commit comments