Skip to content

Commit d980caa

Browse files
Add API auth documentation (#219)
* Update API doc with Auth header format instructions * Update open API docs to use HMAC header
1 parent 69a5f7c commit d980caa

4 files changed

Lines changed: 183 additions & 36 deletions

File tree

docs/reporting-app/api.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,58 @@
33
A number of API endpoints are available under the `/api` route, with interactive
44
documentation at `/api/docs`.
55

6+
## Authentication
7+
8+
All API endpoints under `/api` (except `/api/healthcheck`) require HMAC-SHA256 authentication.
9+
10+
### Header Format
11+
12+
```
13+
Authorization: HMAC sig={base64_signature}
14+
```
15+
16+
### Generating HMAC Signatures
17+
18+
The signature is computed from the raw request body:
19+
20+
**Ruby:**
21+
```ruby
22+
body = { member_id: "123" }.to_json
23+
signature = Base64.strict_encode64(
24+
OpenSSL::HMAC.digest("sha256", ENV["API_SECRET_KEY"], body)
25+
)
26+
```
27+
28+
**Bash (curl POST):**
29+
```bash
30+
BODY='{"member_id":"123"}'
31+
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET_KEY" -binary | base64)
32+
curl -X POST http://localhost:3000/api/certifications \
33+
-H "Content-Type: application/json" \
34+
-H "Authorization: HMAC sig=$SIG" \
35+
-d "$BODY"
36+
```
37+
38+
**Bash (curl GET):**
39+
```bash
40+
# For GET requests (no body), the signature is computed from an empty string
41+
SIG=$(echo -n "" | openssl dgst -sha256 -hmac "$API_SECRET_KEY" -binary | base64)
42+
curl -X GET http://localhost:3000/api/certifications/{id} \
43+
-H "Authorization: HMAC sig=$SIG"
44+
```
45+
46+
### Testing with RSpec
47+
48+
Use the `hmac_auth_headers` helper provided by `Strata::Testing::ApiAuthHelpers`:
49+
50+
```ruby
51+
post api_certifications_url,
52+
params: body,
53+
headers: hmac_auth_headers(body: body, secret: Rails.configuration.api_secret_key)
54+
```
55+
56+
For detailed security architecture, see [API Security Documentation](/docs/architecture/api-security/api-security.md).
57+
658
## Stability
759

860
During the current period of initial active development, there are no stability

reporting-app/config/initializers/oas_rails.rb

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,49 @@
66
config.info.version = "1.0.0"
77
config.info.summary = "System for tracking and certifying community engagement requirements for Medicaid"
88
config.info.description = <<~HEREDOC
9-
# Welcome to the Community Engagement Medicaid API
10-
11-
This is the OpenAPI spec for interacting with the Community Engagement Medicaid API.
12-
13-
## Getting Started
14-
15-
This demo API has no required authentication at the moment.
9+
# Community Engagement Medicaid API
10+
11+
## Authentication
12+
13+
All API endpoints (except `/api/healthcheck`) require HMAC-SHA256 authentication.
14+
15+
### Request Format
16+
```
17+
Authorization: HMAC sig={base64_signature}
18+
```
19+
20+
### Generating the Signature
21+
22+
1. Take the raw JSON request body
23+
2. Compute HMAC-SHA256 using the shared secret key
24+
3. Base64-encode (strict) the result
25+
26+
**Ruby Example:**
27+
```ruby
28+
body = { member_id: "123" }.to_json
29+
signature = Base64.strict_encode64(
30+
OpenSSL::HMAC.digest("sha256", secret_key, body)
31+
)
32+
# Header: Authorization: HMAC sig=\#{signature}
33+
```
34+
35+
**curl Example (POST):**
36+
```bash
37+
BODY='{"member_id":"123"}'
38+
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET_KEY" -binary | base64)
39+
curl -X POST https://api.example.com/api/certifications \\
40+
-H "Content-Type: application/json" \\
41+
-H "Authorization: HMAC sig=$SIG" \\
42+
-d "$BODY"
43+
```
44+
45+
**curl Example (GET):**
46+
```bash
47+
# For GET requests (no body), the signature is computed from an empty string
48+
SIG=$(echo -n "" | openssl dgst -sha256 -hmac "$API_SECRET_KEY" -binary | base64)
49+
curl -X GET https://api.example.com/api/certifications/{id} \\
50+
-H "Authorization: HMAC sig=$SIG"
51+
```
1652
HEREDOC
1753
config.info.contact.name = "Nava PBC"
1854
config.info.contact.email = "medicaid@navapbc.com"
@@ -78,7 +114,7 @@
78114

79115
# Whether to authenticate all routes by default
80116
# Default is true; set to false if you don't want all routes to include security schemas by default
81-
# config.authenticate_all_routes_by_default = true
117+
config.authenticate_all_routes_by_default = true
82118

83119
# Default security schema used for authentication
84120
# Choose a predefined security schema
@@ -89,13 +125,14 @@
89125
# You can uncomment and modify to use custom security schemas
90126
# Please follow the documentation: https://spec.openapis.org/oas/latest.html#security-scheme-object
91127
#
92-
# config.security_schemas = {
93-
# bearer:{
94-
# "type": "apiKey",
95-
# "name": "api_key",
96-
# "in": "header"
97-
# }
98-
# }
128+
config.security_schemas = {
129+
hmac: {
130+
type: "apiKey",
131+
name: "Authorization",
132+
in: "header",
133+
description: "HMAC signature in format: HMAC sig={base64_signature}"
134+
}
135+
}
99136

100137
# ###########################
101138
# Default Responses (Errors)

reporting-app/lib/assets/oas.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
{
22
"components": {
3+
"securitySchemes": {
4+
"hmac": {
5+
"type": "apiKey",
6+
"name": "Authorization",
7+
"in": "header",
8+
"description": "HMAC-SHA256 signature. Format: HMAC sig={base64_encoded_signature}. The signature is computed from the request body using the shared secret key."
9+
}
10+
},
311
"schemas": {
412
"CertificationCreateRequestBody": {
513
"type": "object",

reporting-app/openapi.generated.yml

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
---
22
components:
3+
securitySchemes:
4+
hmac:
5+
type: apiKey
6+
name: Authorization
7+
in: header
8+
description: 'HMAC-SHA256 signature. Format: HMAC sig={base64_encoded_signature}.
9+
The signature is computed from the request body using the shared secret key.'
310
schemas:
411
CertificationCreateRequestBody:
512
type: object
@@ -433,43 +440,43 @@ components:
433440
required:
434441
- status
435442
responses:
436-
e85a7efe856f3b4a33e16865be21f15d:
443+
fce2c5f59cd97181b0cb69aafab2022a:
437444
description: Created Certification.
438445
content:
439446
application/json:
440447
schema:
441448
"$ref": "#/components/schemas/CertificationResponseBody"
442-
6125a62221e212bb815526051661dbdc:
449+
0ea4ab356e2a8b5ccab72cf7a3ce02e1:
443450
description: User error.
444451
content:
445452
application/json:
446453
schema:
447454
"$ref": "#/components/schemas/ErrorResponseBody"
448-
2e75eacc71eb2f0502fa228d762d5a55:
455+
f1ce98ed91709726d022bca82c6ade1a:
449456
description: User error.
450457
content:
451458
application/json:
452459
schema:
453460
"$ref": "#/components/schemas/ErrorResponseBody"
454-
90dc71790b26f81c5e56146f6d9f5018:
461+
3b0ea479d309b77a0516d51bce083571:
455462
description: A Certification
456463
content:
457464
application/json:
458465
schema:
459466
"$ref": "#/components/schemas/CertificationResponseBody"
460-
f03eaaaf493bc30b5c43df958dbb7e8e:
467+
dfa2e176967676c731ccb4a96d74f8f6:
461468
description: Not found.
462469
content:
463470
application/json:
464471
schema:
465472
"$ref": "#/components/schemas/ErrorResponseBody"
466-
d490eeb4ff1770bf36c95bf8931bb69e:
473+
d5f88820bbb77585eef9547c048da107:
467474
description: Response
468475
content:
469476
application/json:
470477
schema:
471478
"$ref": "#/components/schemas/ece4f2c15f241af4536e1132d15a279a"
472-
4a0c71781938225c26730e68f030c36c:
479+
8d921f7b0646bf9b04d277eae3a87fe1:
473480
description: Response
474481
content:
475482
application/json:
@@ -485,7 +492,7 @@ components:
485492
type: string
486493
style: simple
487494
requestBodies:
488-
3ac5de20e865f14a0346ef405d86aca7:
495+
6937948de9fee00ae4778aea1159f62d:
489496
description: The Certification data.
490497
content:
491498
application/json:
@@ -499,7 +506,6 @@ components:
499506
certification_type:
500507
"$ref": "#/components/schemas/CertificationCreateRequestBody/examples/certification_type"
501508
required: false
502-
securitySchemes: {}
503509
headers: {}
504510
examples: {}
505511
links: {}
@@ -510,13 +516,49 @@ info:
510516
summary: System for tracking and certifying community engagement requirements for
511517
Medicaid
512518
description: |
513-
# Welcome to the Community Engagement Medicaid API
519+
# Community Engagement Medicaid API
514520
515-
This is the OpenAPI spec for interacting with the Community Engagement Medicaid API.
521+
## Authentication
516522
517-
## Getting Started
523+
All API endpoints (except `/api/healthcheck`) require HMAC-SHA256 authentication.
518524
519-
This demo API has no required authentication at the moment.
525+
### Request Format
526+
```
527+
Authorization: HMAC sig={base64_signature}
528+
```
529+
530+
### Generating the Signature
531+
532+
1. Take the raw JSON request body
533+
2. Compute HMAC-SHA256 using the shared secret key
534+
3. Base64-encode (strict) the result
535+
536+
**Ruby Example:**
537+
```ruby
538+
body = { member_id: "123" }.to_json
539+
signature = Base64.strict_encode64(
540+
OpenSSL::HMAC.digest("sha256", secret_key, body)
541+
)
542+
# Header: Authorization: HMAC sig=#{signature}
543+
```
544+
545+
**curl Example (POST):**
546+
```bash
547+
BODY='{"member_id":"123"}'
548+
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET_KEY" -binary | base64)
549+
curl -X POST https://api.example.com/api/certifications \
550+
-H "Content-Type: application/json" \
551+
-H "Authorization: HMAC sig=$SIG" \
552+
-d "$BODY"
553+
```
554+
555+
**curl Example (GET):**
556+
```bash
557+
# For GET requests (no body), the signature is computed from an empty string
558+
SIG=$(echo -n "" | openssl dgst -sha256 -hmac "$API_SECRET_KEY" -binary | base64)
559+
curl -X GET https://api.example.com/api/certifications/{id} \
560+
-H "Authorization: HMAC sig=$SIG"
561+
```
520562
termsOfService: ''
521563
contact:
522564
name: Nava PBC
@@ -540,14 +582,16 @@ paths:
540582
description: "#"
541583
operationId: POST__api_certifications
542584
requestBody:
543-
"$ref": "#/components/requestBodies/3ac5de20e865f14a0346ef405d86aca7"
585+
"$ref": "#/components/requestBodies/6937948de9fee00ae4778aea1159f62d"
544586
responses:
545587
'201':
546-
"$ref": "#/components/responses/e85a7efe856f3b4a33e16865be21f15d"
588+
"$ref": "#/components/responses/fce2c5f59cd97181b0cb69aafab2022a"
547589
'400':
548-
"$ref": "#/components/responses/6125a62221e212bb815526051661dbdc"
590+
"$ref": "#/components/responses/0ea4ab356e2a8b5ccab72cf7a3ce02e1"
549591
'422':
550-
"$ref": "#/components/responses/2e75eacc71eb2f0502fa228d762d5a55"
592+
"$ref": "#/components/responses/f1ce98ed91709726d022bca82c6ade1a"
593+
security:
594+
- hmac: []
551595
"/api/certifications/{id}":
552596
get:
553597
tags:
@@ -559,9 +603,11 @@ paths:
559603
- "$ref": "#/components/parameters/49e46dd8af57e26752938ebb0d0ec979"
560604
responses:
561605
'200':
562-
"$ref": "#/components/responses/90dc71790b26f81c5e56146f6d9f5018"
606+
"$ref": "#/components/responses/3b0ea479d309b77a0516d51bce083571"
563607
'404':
564-
"$ref": "#/components/responses/f03eaaaf493bc30b5c43df958dbb7e8e"
608+
"$ref": "#/components/responses/dfa2e176967676c731ccb4a96d74f8f6"
609+
security:
610+
- hmac: []
565611
"/api/health":
566612
get:
567613
tags:
@@ -571,6 +617,10 @@ paths:
571617
operationId: GET__api_health
572618
responses:
573619
'200':
574-
"$ref": "#/components/responses/d490eeb4ff1770bf36c95bf8931bb69e"
620+
"$ref": "#/components/responses/d5f88820bbb77585eef9547c048da107"
575621
'503':
576-
"$ref": "#/components/responses/4a0c71781938225c26730e68f030c36c"
622+
"$ref": "#/components/responses/8d921f7b0646bf9b04d277eae3a87fe1"
623+
security:
624+
- hmac: []
625+
security:
626+
- hmac: []

0 commit comments

Comments
 (0)