git.iko.soy/rowtype-yoga/purescript-yoga-http-api
on feat/custom-type-errors help sign in
feat: Add support for multiple authentication types (BasicAuth, ApiKey, DigestAuth)
40ec001Mark
@@ -5,3 +5,4 @@ output-test
5node_modules
5node_modules
6.purs-repl
6.purs-repl
7.psc-ide-port
7.psc-ide-port
8+.psci_modules
src/Yoga/HTTP/API/Route.purs+2
@@ -5,6 +5,7 @@ module Yoga.HTTP.API.Route
5 , module Yoga.HTTP.API.Route.StatusCode
5 , module Yoga.HTTP.API.Route.StatusCode
6 , module Yoga.HTTP.API.Route.HeaderError
6 , module Yoga.HTTP.API.Route.HeaderError
7 , module Yoga.HTTP.API.Route.HeaderValue
7 , module Yoga.HTTP.API.Route.HeaderValue
8+ , module Yoga.HTTP.API.Route.Auth
8 , module Yoga.HTTP.API.Route.BearerToken
9 , module Yoga.HTTP.API.Route.BearerToken
9 , module Yoga.HTTP.API.Route.Response
10 , module Yoga.HTTP.API.Route.Response
10 , module Yoga.HTTP.API.Route.Handler
11 , module Yoga.HTTP.API.Route.Handler
@@ -15,6 +16,7 @@ module Yoga.HTTP.API.Route
15 ) where
16 ) where
16
17
17import Yoga.HTTP.API.Path (Path, Root, Lit, Capture, PathCons, Param, QueryParams, Required, type (/), type (:), type (:?), class PathPattern, pathPattern, class ParseParam, parseParam, class ParsePath, parsePath)
18import Yoga.HTTP.API.Path (Path, Root, Lit, Capture, PathCons, Param, QueryParams, Required, type (/), type (:), type (:?), class PathPattern, pathPattern, class ParseParam, parseParam, class ParsePath, parsePath)
19+import Yoga.HTTP.API.Route.Auth (BearerToken(..), BasicAuth(..), ApiKeyHeader(..), DigestAuth(..))
18import Yoga.HTTP.API.Route.BearerToken (BearerToken(..))
20import Yoga.HTTP.API.Route.BearerToken (BearerToken(..))
19import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
21import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
20import Yoga.HTTP.API.Route.Handler (HandlerFn, Request)
22import Yoga.HTTP.API.Route.Handler (HandlerFn, Request)
src/Yoga/HTTP/API/Route/Auth.pursnew file+114
@@ -0,0 +1,114 @@
1+module Yoga.HTTP.API.Route.Auth
2+ ( BearerToken(..)
3+ , BasicAuth(..)
4+ , ApiKeyHeader(..)
5+ , DigestAuth(..)
6+ ) where
7+
8+import Prelude
9+
10+import Data.Either (Either(..))
11+import Data.Newtype (class Newtype)
12+import Data.String as String
13+import Yoga.HTTP.API.Route.HeaderValue (class HeaderValue, class HeaderValueType)
14+
15+--------------------------------------------------------------------------------
16+-- BearerToken
17+--------------------------------------------------------------------------------
18+
19+-- | Bearer token newtype that validates the "Bearer " prefix
20+-- |
21+-- | Example:
22+-- | parseHeader "Bearer abc123" :: Either String BearerToken
23+-- | = Right (BearerToken "abc123")
24+-- | parseHeader "abc123" :: Either String BearerToken
25+-- | = Left "missing 'Bearer ' prefix"
26+newtype BearerToken = BearerToken String
27+
28+derive instance Eq BearerToken
29+derive newtype instance Show BearerToken
30+derive instance Newtype BearerToken _
31+
32+instance HeaderValue BearerToken where
33+ parseHeader s =
34+ if String.take 7 s == "Bearer " then Right $ BearerToken $ String.drop 7 s
35+ else Left "missing 'Bearer ' prefix"
36+
37+ printHeader (BearerToken token) = "Bearer " <> token
38+
39+instance HeaderValueType BearerToken where
40+ headerValueType _ = "string"
41+
42+--------------------------------------------------------------------------------
43+-- BasicAuth
44+--------------------------------------------------------------------------------
45+
46+-- | Basic authentication header value (Base64-encoded credentials)
47+-- |
48+-- | Example:
49+-- | parseHeader "Basic dXNlcjpwYXNz" :: Either String BasicAuth
50+-- | = Right (BasicAuth "dXNlcjpwYXNz")
51+-- |
52+-- | The raw Base64-encoded credentials are preserved for server-side verification
53+newtype BasicAuth = BasicAuth String
54+
55+derive instance Eq BasicAuth
56+derive newtype instance Show BasicAuth
57+derive instance Newtype BasicAuth _
58+
59+instance HeaderValue BasicAuth where
60+ parseHeader s =
61+ if String.take 6 s == "Basic " then Right $ BasicAuth $ String.drop 6 s
62+ else Left "missing 'Basic ' prefix"
63+
64+ printHeader (BasicAuth credentials) = "Basic " <> credentials
65+
66+instance HeaderValueType BasicAuth where
67+ headerValueType _ = "string"
68+
69+--------------------------------------------------------------------------------
70+-- ApiKeyHeader
71+--------------------------------------------------------------------------------
72+
73+-- | API Key passed in a custom header
74+-- |
75+-- | Example:
76+-- | parseHeader "abc123" :: Either String ApiKeyHeader
77+-- | = Right (ApiKeyHeader "abc123")
78+newtype ApiKeyHeader = ApiKeyHeader String
79+
80+derive instance Eq ApiKeyHeader
81+derive newtype instance Show ApiKeyHeader
82+derive instance Newtype ApiKeyHeader _
83+
84+instance HeaderValue ApiKeyHeader where
85+ parseHeader = Right <<< ApiKeyHeader
86+ printHeader (ApiKeyHeader key) = key
87+
88+instance HeaderValueType ApiKeyHeader where
89+ headerValueType _ = "string"
90+
91+--------------------------------------------------------------------------------
92+-- DigestAuth
93+--------------------------------------------------------------------------------
94+
95+-- | Digest authentication header value
96+-- |
97+-- | Example:
98+-- | parseHeader "Digest username=\"user\", realm=\"api\", ..." :: Either String DigestAuth
99+-- | = Right (DigestAuth "Digest username=\"user\", realm=\"api\", ...")
100+newtype DigestAuth = DigestAuth String
101+
102+derive instance Eq DigestAuth
103+derive newtype instance Show DigestAuth
104+derive instance Newtype DigestAuth _
105+
106+instance HeaderValue DigestAuth where
107+ parseHeader s =
108+ if String.take 7 s == "Digest " then Right $ DigestAuth s
109+ else Left "missing 'Digest ' prefix"
110+
111+ printHeader (DigestAuth value) = value
112+
113+instance HeaderValueType DigestAuth where
114+ headerValueType _ = "string"
src/Yoga/HTTP/API/Route/BearerToken.purs+2−34
@@ -1,37 +1,5 @@
1module Yoga.HTTP.API.Route.BearerToken
1module Yoga.HTTP.API.Route.BearerToken
2− ( BearerToken(..)
2+ ( module Exports
3 ) where
3 ) where
4
4
5−import Prelude
5+import Yoga.HTTP.API.Route.Auth (BearerToken(..)) as Exports
6−
7−import Data.Either (Either(..))
8−import Data.Newtype (class Newtype)
9−import Data.String as String
10−import Yoga.HTTP.API.Route.HeaderValue (class HeaderValue, class HeaderValueType)
11−
12−--------------------------------------------------------------------------------
13−-- BearerToken
14−--------------------------------------------------------------------------------
15−
16−-- | Bearer token newtype that validates the "Bearer " prefix
17−-- |
18−-- | Example:
19−-- | parseHeader "Bearer abc123" :: Either String BearerToken
20−-- | = Right (BearerToken "abc123")
21−-- | parseHeader "abc123" :: Either String BearerToken
22−-- | = Left "missing 'Bearer ' prefix"
23−newtype BearerToken = BearerToken String
24−
25−derive instance Eq BearerToken
26−derive newtype instance Show BearerToken
27−derive instance Newtype BearerToken _
28−
29−instance HeaderValue BearerToken where
30− parseHeader s =
31− if String.take 7 s == "Bearer " then Right $ BearerToken $ String.drop 7 s
32− else Left "missing 'Bearer ' prefix"
33−
34− printHeader (BearerToken token) = "Bearer " <> token
35−
36−instance HeaderValueType BearerToken where
37− headerValueType _ = "string"
src/Yoga/HTTP/API/Route/OpenAPI.purs+192−14
@@ -66,8 +66,10 @@ module Yoga.HTTP.API.Route.OpenAPI
66
66
67import Prelude
67import Prelude
68
68
69+import Data.Array as Array
69import Data.Foldable (foldl)
70import Data.Foldable (foldl)
70import Data.Maybe (Maybe(..), maybe)
71import Data.Maybe (Maybe(..), maybe)
72+import Data.String as String
71import Data.String.Regex (replace)
73import Data.String.Regex (replace)
72import Data.String.Regex.Flags (global)
74import Data.String.Regex.Flags (global)
73import Data.String.Regex.Unsafe (unsafeRegex)
75import Data.String.Regex.Unsafe (unsafeRegex)
@@ -80,7 +82,7 @@ import Prim.RowList (class RowToList, RowList, Cons, Nil)
80import Prim.RowList as RL
82import Prim.RowList as RL
81import Type.Proxy (Proxy(..))
83import Type.Proxy (Proxy(..))
82import Unsafe.Coerce (unsafeCoerce)
84import Unsafe.Coerce (unsafeCoerce)
83−import Yoga.HTTP.API.Route.BearerToken (BearerToken)
85+import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, DigestAuth)
84import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
86import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
85import Yoga.HTTP.API.Route.HeaderValue (class HeaderValueType, headerValueType)
87import Yoga.HTTP.API.Route.HeaderValue (class HeaderValueType, headerValueType)
86import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Format, Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Nullable, Default, Deprecated, Enum, Schema, class HasDescription, description, class HasExample, example, class HasFormat, format, class HasDeprecated, deprecated, class HasMinimum, minimum, class HasMaximum, maximum, class HasPattern, pattern, class HasMinLength, minLength, class HasMaxLength, maxLength, class HasTitle, title, class HasNullable, nullable, class HasDefault, default, class HasEnum, enum, class HasOperationMetadata, operationMetadata)
88import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Format, Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Nullable, Default, Deprecated, Enum, Schema, class HasDescription, description, class HasExample, example, class HasFormat, format, class HasDeprecated, deprecated, class HasMinimum, minimum, class HasMaximum, maximum, class HasPattern, pattern, class HasMinLength, minLength, class HasMaxLength, maxLength, class HasTitle, title, class HasNullable, nullable, class HasDefault, default, class HasEnum, enum, class HasOperationMetadata, operationMetadata)
@@ -187,6 +189,54 @@ else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex Bea
187else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated BearerToken) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated BearerToken) tail) headers where
189else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated BearerToken) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated BearerToken) tail) headers where
188 renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
190 renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
189
191
192+-- Skip BasicAuth headers (they're handled as security schemes, not regular headers)
193+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name BasicAuth tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name BasicAuth tail) headers where
194+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
195+
196+-- Skip BasicAuth wrapped in Description
197+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Description desc BasicAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Description desc BasicAuth) tail) headers where
198+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
199+
200+-- Skip BasicAuth wrapped in Example
201+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex BasicAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Example ex BasicAuth) tail) headers where
202+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
203+
204+-- Skip BasicAuth wrapped in Deprecated
205+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated BasicAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated BasicAuth) tail) headers where
206+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
207+
208+-- Skip ApiKeyHeader headers (they're handled as security schemes, not regular headers)
209+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name ApiKeyHeader tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name ApiKeyHeader tail) headers where
210+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
211+
212+-- Skip ApiKeyHeader wrapped in Description
213+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Description desc ApiKeyHeader) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Description desc ApiKeyHeader) tail) headers where
214+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
215+
216+-- Skip ApiKeyHeader wrapped in Example
217+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex ApiKeyHeader) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Example ex ApiKeyHeader) tail) headers where
218+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
219+
220+-- Skip ApiKeyHeader wrapped in Deprecated
221+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated ApiKeyHeader) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated ApiKeyHeader) tail) headers where
222+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
223+
224+-- Skip DigestAuth headers (they're handled as security schemes, not regular headers)
225+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name DigestAuth tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name DigestAuth tail) headers where
226+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
227+
228+-- Skip DigestAuth wrapped in Description
229+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Description desc DigestAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Description desc DigestAuth) tail) headers where
230+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
231+
232+-- Skip DigestAuth wrapped in Example
233+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex DigestAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Example ex DigestAuth) tail) headers where
234+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
235+
236+-- Skip DigestAuth wrapped in Deprecated
237+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated DigestAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated DigestAuth) tail) headers where
238+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
239+
190-- Required header (non-Maybe type)
240-- Required header (non-Maybe type)
191else instance
241else instance
192 ( IsSymbol name
242 ( IsSymbol name
@@ -290,6 +340,106 @@ else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecate
290 in
340 in
291 [ bearerAuth ]
341 [ bearerAuth ]
292
342
343+-- Case: BasicAuth found
344+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name BasicAuth tail) where
345+ detectSecurityRL _ =
346+ let
347+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
348+ in
349+ [ basicAuth ]
350+
351+-- Case: BasicAuth wrapped in Description
352+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Description desc BasicAuth) tail) where
353+ detectSecurityRL _ =
354+ let
355+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
356+ in
357+ [ basicAuth ]
358+
359+-- Case: BasicAuth wrapped in Example
360+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Example ex BasicAuth) tail) where
361+ detectSecurityRL _ =
362+ let
363+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
364+ in
365+ [ basicAuth ]
366+
367+-- Case: BasicAuth wrapped in Deprecated
368+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecated BasicAuth) tail) where
369+ detectSecurityRL _ =
370+ let
371+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
372+ in
373+ [ basicAuth ]
374+
375+-- Case: ApiKeyHeader found
376+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name ApiKeyHeader tail) where
377+ detectSecurityRL _ =
378+ let
379+ keyName = reflectSymbol (Proxy :: Proxy name)
380+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
381+ in
382+ [ apiKeyAuth ]
383+
384+-- Case: ApiKeyHeader wrapped in Description
385+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Description desc ApiKeyHeader) tail) where
386+ detectSecurityRL _ =
387+ let
388+ keyName = reflectSymbol (Proxy :: Proxy name)
389+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
390+ in
391+ [ apiKeyAuth ]
392+
393+-- Case: ApiKeyHeader wrapped in Example
394+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Example ex ApiKeyHeader) tail) where
395+ detectSecurityRL _ =
396+ let
397+ keyName = reflectSymbol (Proxy :: Proxy name)
398+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
399+ in
400+ [ apiKeyAuth ]
401+
402+-- Case: ApiKeyHeader wrapped in Deprecated
403+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Deprecated ApiKeyHeader) tail) where
404+ detectSecurityRL _ =
405+ let
406+ keyName = reflectSymbol (Proxy :: Proxy name)
407+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
408+ in
409+ [ apiKeyAuth ]
410+
411+-- Case: DigestAuth found
412+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name DigestAuth tail) where
413+ detectSecurityRL _ =
414+ let
415+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
416+ in
417+ [ digestAuth ]
418+
419+-- Case: DigestAuth wrapped in Description
420+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Description desc DigestAuth) tail) where
421+ detectSecurityRL _ =
422+ let
423+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
424+ in
425+ [ digestAuth ]
426+
427+-- Case: DigestAuth wrapped in Example
428+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Example ex DigestAuth) tail) where
429+ detectSecurityRL _ =
430+ let
431+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
432+ in
433+ [ digestAuth ]
434+
435+-- Case: DigestAuth wrapped in Deprecated
436+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecated DigestAuth) tail) where
437+ detectSecurityRL _ =
438+ let
439+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
440+ in
441+ [ digestAuth ]
442+
293-- Case: Non-security header, recurse
443-- Case: Non-security header, recurse
294else instance (DetectSecurityRL tail, HeaderValueType ty) => DetectSecurityRL (RL.Cons name ty tail) where
444else instance (DetectSecurityRL tail, HeaderValueType ty) => DetectSecurityRL (RL.Cons name ty tail) where
295 detectSecurityRL _ = detectSecurityRL (Proxy :: Proxy tail)
445 detectSecurityRL _ = detectSecurityRL (Proxy :: Proxy tail)
@@ -1237,32 +1387,60 @@ foreign import data OpenAPISpec :: Type
1237instance WriteForeign OpenAPISpec where
1387instance WriteForeign OpenAPISpec where
1238 writeImpl = unsafeCoerce
1388 writeImpl = unsafeCoerce
1239
1389
1240−-- | Detect if any operations use bearer token authentication
1390+-- | Detect which security schemes are used across all operations
1241-- | and build the securitySchemes component
1391-- | and build the securitySchemes component
1242buildSecuritySchemes :: Array OperationEntry -> FObject.Object Foreign
1392buildSecuritySchemes :: Array OperationEntry -> FObject.Object Foreign
1243buildSecuritySchemes ops =
1393buildSecuritySchemes ops =
1244 let
1394 let
1245− hasSecurity = ops # foldl checkOp false
1395+ securityKeys = ops # foldl collectSecurityKeys []
1246 where
1396 where
1247− checkOp acc entry =
1397+ collectSecurityKeys acc entry =
1248 let
1398 let
1249 opObj = unsafeCoerce (unsafeParseJSON entry.operation) :: FObject.Object Foreign
1399 opObj = unsafeCoerce (unsafeParseJSON entry.operation) :: FObject.Object Foreign
1250 securityField = FObject.lookup "security" opObj
1400 securityField = FObject.lookup "security" opObj
1251 in
1401 in
1252− acc || case securityField of
1402+ case securityField of
1253− Nothing -> false
1403+ Nothing -> acc
1254− Just _ -> true
1404+ Just secArray ->
1405+ let
1406+ secList = unsafeCoerce secArray :: Array (FObject.Object Foreign)
1407+ keys = secList >>= FObject.keys
1255− in
1408+ in
1256− if hasSecurity then
1409+ acc <> keys
1257− FObject.singleton "bearerAuth"
1410+
1411+ uniqueKeys = Array.nub securityKeys
1412+ schemes = uniqueKeys <#> \key ->
1413+ let
1414+ scheme = case key of
1258− ( unsafeCoerce
1415+ "bearerAuth" -> Just $ Tuple key $ unsafeCoerce
1259 { type: "http"
1416 { type: "http"
1260 , scheme: "bearer"
1417 , scheme: "bearer"
1261 , bearerFormat: "JWT"
1418 , bearerFormat: "JWT"
1262 }
1419 }
1263− )
1420+ "basicAuth" -> Just $ Tuple key $ unsafeCoerce
1421+ { type: "http"
1422+ , scheme: "basic"
1423+ }
1424+ "digestAuth" -> Just $ Tuple key $ unsafeCoerce
1425+ { type: "http"
1426+ , scheme: "digest"
1427+ }
1428+ _ ->
1429+ -- API Key (header names ending with "ApiKey")
1430+ if String.take (String.length key - 6) key /= "" && String.drop (String.length key - 6) key == "ApiKey" then
1431+ let
1432+ headerName = String.take (String.length key - 6) key
1433+ in
1434+ Just $ Tuple key $ unsafeCoerce
1435+ { type: "apiKey"
1436+ , in: "header"
1437+ , name: headerName
1438+ }
1264− else
1439+ else Nothing
1265− FObject.empty
1440+ in
1441+ scheme
1442+ in
1443+ FObject.fromFoldable $ Array.catMaybes schemes
1266
1444
1267-- | Type for OpenAPI server object
1445-- | Type for OpenAPI server object
1268type ServerObject =
1446type ServerObject =
test/BearerAuthTest.Spec.purs+107−14
@@ -5,42 +5,135 @@ import Prelude
5import Data.String as String
5import Data.String as String
6import Effect (Effect)
6import Effect (Effect)
7import Yoga.HTTP.API.Path (Path, Lit)
7import Yoga.HTTP.API.Path (Path, Lit)
8−import Yoga.HTTP.API.Route (GET, Route, Request, BearerToken, buildOpenAPISpec)
8+import Yoga.HTTP.API.Route (GET, Route, Request, buildOpenAPISpec)
9+import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, DigestAuth)
9import Yoga.JSON (writeJSON)
10import Yoga.JSON (writeJSON)
10import ViTest (ViTest, describe, test)
11import ViTest (ViTest, describe, test)
11import ViTest.Expect (expectToBe)
12import ViTest.Expect (expectToBe)
12
13
13--------------------------------------------------------------------------------
14--------------------------------------------------------------------------------
14−-- Test API with BearerToken
15+-- Test APIs with different authentication types
15--------------------------------------------------------------------------------
16--------------------------------------------------------------------------------
16
17
17−type AuthAPI =
18+type BearerAuthAPI =
18 { getProtected :: Route GET (Path (Lit "protected")) (Request { headers :: { authorization :: BearerToken } }) (ok :: { body :: { message :: String } })
19 { getProtected :: Route GET (Path (Lit "protected")) (Request { headers :: { authorization :: BearerToken } }) (ok :: { body :: { message :: String } })
19 }
20 }
20
21
22+type BasicAuthAPI =
23+ { getProtected :: Route GET (Path (Lit "protected")) (Request { headers :: { authorization :: BasicAuth } }) (ok :: { body :: { message :: String } })
24+ }
25+
26+type ApiKeyAPI =
27+ { getProtected :: Route GET (Path (Lit "protected")) (Request { headers :: { apiKey :: ApiKeyHeader } }) (ok :: { body :: { message :: String } })
28+ }
29+
30+type DigestAuthAPI =
31+ { getProtected :: Route GET (Path (Lit "protected")) (Request { headers :: { authorization :: DigestAuth } }) (ok :: { body :: { message :: String } })
32+ }
33+
34+type MultiAuthAPI =
35+ { bearerEndpoint :: Route GET (Path (Lit "bearer")) (Request { headers :: { authorization :: BearerToken } }) (ok :: { body :: { message :: String } })
36+ , basicEndpoint :: Route GET (Path (Lit "basic")) (Request { headers :: { authorization :: BasicAuth } }) (ok :: { body :: { message :: String } })
37+ , apiKeyEndpoint :: Route GET (Path (Lit "apikey")) (Request { headers :: { xApiKey :: ApiKeyHeader } }) (ok :: { body :: { message :: String } })
38+ }
39+
21--------------------------------------------------------------------------------
40--------------------------------------------------------------------------------
22-- Tests
41-- Tests
23--------------------------------------------------------------------------------
42--------------------------------------------------------------------------------
24
43
25−testBearerTokenNotInHeaders :: Effect ViTest
44+testBearerTokenAuth :: Effect ViTest
26−testBearerTokenNotInHeaders = describe "BearerToken in OpenAPI" do
45+testBearerTokenAuth = describe "BearerToken Authentication" do
27 _ <- test "BearerToken does not appear as a regular header parameter" do
46 _ <- test "BearerToken does not appear as a regular header parameter" do
28− let spec = buildOpenAPISpec @AuthAPI { title: "Auth API", version: "1.0.0" }
47+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
29 let json = writeJSON spec
48 let json = writeJSON spec
30− -- Authorization should NOT appear in parameters array
31 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
49 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
32 expectToBe false hasAuthParam
50 expectToBe false hasAuthParam
33
51
34 _ <- test "BearerToken appears in security requirements" do
52 _ <- test "BearerToken appears in security requirements" do
35− let spec = buildOpenAPISpec @AuthAPI { title: "Auth API", version: "1.0.0" }
53+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
36 let json = writeJSON spec
54 let json = writeJSON spec
37− -- Should contain bearerAuth security reference
38 let hasBearerAuth = String.contains (String.Pattern "bearerAuth") json
55 let hasBearerAuth = String.contains (String.Pattern "bearerAuth") json
39 expectToBe true hasBearerAuth
56 expectToBe true hasBearerAuth
40
57
41− test "OpenAPI spec contains securitySchemes component" do
58+ test "BearerToken security scheme has correct structure" do
59+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
60+ let json = writeJSON spec
61+ expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
62+ expectToBe true (String.contains (String.Pattern "\"scheme\":\"bearer\"") json)
63+
64+testBasicAuth :: Effect ViTest
65+testBasicAuth = describe "BasicAuth Authentication" do
66+ _ <- test "BasicAuth does not appear as a regular header parameter" do
42− let spec = buildOpenAPISpec @AuthAPI { title: "Auth API", version: "1.0.0" }
67+ let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0" }
68+ let json = writeJSON spec
69+ let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
70+ expectToBe false hasAuthParam
71+
72+ _ <- test "BasicAuth appears in security requirements" do
73+ let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0" }
74+ let json = writeJSON spec
75+ let hasBasicAuth = String.contains (String.Pattern "basicAuth") json
76+ expectToBe true hasBasicAuth
77+
78+ test "BasicAuth security scheme has correct structure" do
79+ let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0" }
80+ let json = writeJSON spec
81+ expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
82+ expectToBe true (String.contains (String.Pattern "\"scheme\":\"basic\"") json)
83+
84+testApiKeyAuth :: Effect ViTest
85+testApiKeyAuth = describe "ApiKey Authentication" do
86+ _ <- test "ApiKeyHeader does not appear as a regular header parameter" do
87+ let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0" }
88+ let json = writeJSON spec
89+ -- Check that parameters array is empty (no header parameters)
90+ let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
91+ expectToBe true hasEmptyParams
92+
93+ _ <- test "ApiKey appears in security requirements" do
94+ let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0" }
95+ let json = writeJSON spec
96+ let hasApiKey = String.contains (String.Pattern "apiKeyApiKey") json
97+ expectToBe true hasApiKey
98+
99+ test "ApiKey security scheme has correct structure" do
100+ let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0" }
101+ let json = writeJSON spec
102+ expectToBe true (String.contains (String.Pattern "\"type\":\"apiKey\"") json)
103+ expectToBe true (String.contains (String.Pattern "\"in\":\"header\"") json)
104+
105+testDigestAuth :: Effect ViTest
106+testDigestAuth = describe "DigestAuth Authentication" do
107+ _ <- test "DigestAuth does not appear as a regular header parameter" do
108+ let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0" }
109+ let json = writeJSON spec
110+ let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
111+ expectToBe false hasAuthParam
112+
113+ _ <- test "DigestAuth appears in security requirements" do
114+ let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0" }
115+ let json = writeJSON spec
116+ let hasDigestAuth = String.contains (String.Pattern "digestAuth") json
117+ expectToBe true hasDigestAuth
118+
119+ test "DigestAuth security scheme has correct structure" do
120+ let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0" }
121+ let json = writeJSON spec
122+ expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
123+ expectToBe true (String.contains (String.Pattern "\"scheme\":\"digest\"") json)
124+
125+testMultipleAuthTypes :: Effect ViTest
126+testMultipleAuthTypes = describe "Multiple Authentication Types" do
127+ _ <- test "All auth types appear in security schemes" do
128+ let spec = buildOpenAPISpec @MultiAuthAPI { title: "Auth API", version: "1.0.0" }
129+ let json = writeJSON spec
130+ expectToBe true (String.contains (String.Pattern "bearerAuth") json)
131+ expectToBe true (String.contains (String.Pattern "basicAuth") json)
132+ expectToBe true (String.contains (String.Pattern "xApiKeyApiKey") json)
133+
134+ test "None of the auth headers appear as regular parameters" do
135+ let spec = buildOpenAPISpec @MultiAuthAPI { title: "Auth API", version: "1.0.0" }
43 let json = writeJSON spec
136 let json = writeJSON spec
44− -- Should have bearerAuth defined in components.securitySchemes
137+ -- Check that all endpoints have empty parameters arrays
45− let hasSecuritySchemes = String.contains (String.Pattern "securitySchemes") json
138+ let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
46− expectToBe true hasSecuritySchemes
139+ expectToBe true hasEmptyParams
test/Test.Operators.purs+5−1
@@ -39,7 +39,11 @@ spec = do
39 _ <- APIRecordTest.testOperationIds
39 _ <- APIRecordTest.testOperationIds
40 _ <- APIRecordTest.testBuildOpenAPISpec
40 _ <- APIRecordTest.testBuildOpenAPISpec
41 _ <- APIRecordTest.testAPIHandlers
41 _ <- APIRecordTest.testAPIHandlers
42− _ <- BearerAuthTest.testBearerTokenNotInHeaders
42+ _ <- BearerAuthTest.testBearerTokenAuth
43+ _ <- BearerAuthTest.testBasicAuth
44+ _ <- BearerAuthTest.testApiKeyAuth
45+ _ <- BearerAuthTest.testDigestAuth
46+ _ <- BearerAuthTest.testMultipleAuthTypes
43 SchemaComponentTest.testSchemaComponents
47 SchemaComponentTest.testSchemaComponents
44
48
45main :: ViTest
49main :: ViTest