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