Skip to content

Commit e69c12f

Browse files
authored
Adding RBAC for AuthZ (awslabs#8)
Why? Customer will want more AuthZ than just a access to all or none. This introduces RBAC that enables roles to be assigned to Interactions and Resources. Changes: * Created configuration for RBAC (RBACRules.ts) * Bundles AuthZ is in two steps (preliminary check & then again on the resource side) * Changing cognito to be OAuth2 endpoints * To hit the endpoint you must ask for profile & openid or just aws.cognito.signin.user.admin scopes Test: * Created/updated tests * sls deploy locally
1 parent 881b105 commit e69c12f

28 files changed

+711
-102
lines changed

Diff for: README.md

+9-10
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ To change what this FHIR server supports please check out the [config.ts](src/co
2525

2626
### Architecture
2727

28-
The system architecture consists of multiple layers of AWS serverless services. The endpoints are hosted using API Gateway. The database and storage layer consists of Amazon DynamoDB and S3, with ElasticSearch as the search index for the data written to DynamoDB. The endpoints are secured by Cognito for user-level authentication (and future authorization), with API keys for anonymous service level access. The diagram below shows the FHIR server’s system architecture components and how they are related.
28+
The system architecture consists of multiple layers of AWS serverless services. The endpoints are hosted using API Gateway. The database and storage layer consists of Amazon DynamoDB and S3, with ElasticSearch as the search index for the data written to DynamoDB. The endpoints are secured by Cognito for user-level authentication and user-group authorization, with API keys for anonymous service level access. The diagram below shows the FHIR server’s system architecture components and how they are related.
2929
![Architecture](resources/architecture.png)
3030

3131
## Prerequisites
@@ -213,7 +213,7 @@ From the command’s output note down the following data
213213

214214
### Initialize Cognito
215215

216-
Initially, AWS Cognito must be set up with default user credentials in order to support authentication. Without this step, the APIs won’t be accessible through user authentication.
216+
Initially, AWS Cognito is set up supporting OAuth2 requests in order to support authentication and authorization. When first created there will be no users. This step creates a `workshopuser` and assigns the user to the `practitioner` User Group.
217217

218218
Execute the following command substituting all variables with previously noted
219219
values:
@@ -237,7 +237,7 @@ For Mac:
237237
AWS_ACCESS_KEY_ID=<ACCESS_KEY> AWS_SECRET_ACCESS_KEY=<SECRET_KEY> python3 scripts/provision-user.py <USER_POOL_ID> <USER_POOL_APP_CLIENT_ID> <REGION>
238238
```
239239

240-
This will create a user in your Cognito User Pool. The return value will be a token that can be used for authentication with the FHIR API.
240+
This will create a user in your Cognito User Pool. The return value will be an access token that can be used for authentication with the FHIR API.
241241

242242
### Accessing ElasticSearch Kibana Server
243243

@@ -250,15 +250,15 @@ In order to be able to access the Kibana server for your ElasticSearch Service I
250250
```sh
251251
# Find ELASTIC_SEARCH_KIBANA_USER_POOL_APP_CLIENT_ID in the printout
252252
serverless info --verbose
253-
254-
# Create new user
253+
254+
# Create new user
255255
aws cognito-idp sign-up \
256256
--region <REGION> \
257257
--client-id <ELASTIC_SEARCH_KIBANA_USER_POOL_APP_CLIENT_ID> \
258258
--username <[email protected]> \
259259
--password <TEMP_PASSWORD> \
260260
--user-attributes Name="email",Value="<[email protected]>"
261-
261+
262262
# Find ELASTIC_SEARCH_KIBANA_USER_POOL_ID in the printout
263263
# Notice this is a different ID from the one used in the last step
264264
serverless info --verbose
@@ -281,8 +281,7 @@ aws cognito-idp admin-confirm-sign-up \
281281
--user-pool-id us-west-2_sOmeStRing \
282282
--username [email protected] \
283283
--region us-west-2
284-
285-
```
284+
```
286285

287286
#### Get Kibana Url
288287

@@ -304,7 +303,7 @@ aws cloudformation create-stack --stack-name fhir-server-backups --template-body
304303

305304
## Usage Instructions
306305

307-
### Retrieving an authentication token
306+
### Retrieving an access token
308307

309308
In order to access the FHIR API, a COGNITO_AUTH_TOKEN is required. This can be obtained using the following command substituting all variables with previously noted values
310309

@@ -352,7 +351,7 @@ Instructions for importing the environment JSON is located [here](https://thinks
352351
- Fhir_Dev_Env.json
353352
- Fhir_Prod_Env.json
354353

355-
The COGNITO*AUTH_TOKEN required for each of these files can be obtained by following the instructions under [Retrieving an authentication token](#retrieving-an-authentication-token).
354+
The COGNITO_AUTH_TOKEN required for each of these files can be obtained by following the instructions under [Retrieving an authentication token](#retrieving-an-authentication-token).
356355
Other parameters required can be found by running `serverless info --verbose`
357356

358357
### Accessing Binary resources

Diff for: cloudformation/cognito.yaml

+27-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ Resources:
22
UserPool:
33
Type: AWS::Cognito::UserPool
44
Properties:
5+
AccountRecoverySetting:
6+
RecoveryMechanisms:
7+
- Name: verified_email
8+
Priority: 1
9+
AdminCreateUserConfig:
10+
AllowAdminCreateUserOnly: true
511
AutoVerifiedAttributes:
612
- email
713
UserPoolName: !Ref AWS::StackName
@@ -14,12 +20,30 @@ Resources:
1420
UserPoolClient:
1521
Type: AWS::Cognito::UserPoolClient
1622
Properties:
23+
AllowedOAuthFlows:
24+
- code
25+
- implicit
26+
AllowedOAuthFlowsUserPoolClient: true
27+
AllowedOAuthScopes:
28+
- email
29+
- openid
30+
- profile
1731
ClientName: !Sub '${AWS::StackName}-UserPool'
18-
GenerateSecret: false
1932
UserPoolId: !Ref UserPool
33+
CallbackURLs:
34+
- ${self:custom.oauthCallback}
35+
DefaultRedirectURI: ${self:custom.oauthRedirect}
2036
ExplicitAuthFlows:
21-
- ADMIN_NO_SRP_AUTH
22-
- USER_PASSWORD_AUTH
37+
- ALLOW_USER_PASSWORD_AUTH
38+
- ALLOW_REFRESH_TOKEN_AUTH
39+
SupportedIdentityProviders:
40+
- COGNITO
41+
PreventUserExistenceErrors: ENABLED
42+
UserPoolDomain:
43+
Type: AWS::Cognito::UserPoolDomain
44+
Properties:
45+
Domain: !Ref UserPoolClient
46+
UserPoolId: !Ref UserPool
2347
PractitionerUserGroup:
2448
Type: AWS::Cognito::UserPoolGroup
2549
Properties:

Diff for: package.json

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"errorhandler": "^1.5.1",
3131
"express": "^4.17.1",
3232
"flat": "^5.0.0",
33+
"jsonwebtoken": "^8.5.1",
3334
"lodash": "^4.17.15",
3435
"mime-types": "^2.1.26",
3536
"serverless-http": "^2.3.1",
@@ -43,6 +44,7 @@
4344
"@types/node": "^12",
4445
"@types/sinon": "^9.0.0",
4546
"@types/uuid": "^3.4.7",
47+
"@types/jsonwebtoken": "^8.5.0",
4648
"@typescript-eslint/eslint-plugin": "^2.18.0",
4749
"@typescript-eslint/parser": "^2.18.0",
4850
"aws-sdk-mock": "^5.1.0",

Diff for: sampleData/r3FhirConfigWithExclusions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const config: FhirConfig = {
55
orgName: 'Organization Name',
66
auth: {
77
strategy: {
8-
cognito: true,
8+
oauthUrl: 'http://example.com',
99
},
1010
},
1111
server: {

Diff for: sampleData/r4FhirConfigGeneric.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const config: FhirConfig = {
55
orgName: 'Organization Name',
66
auth: {
77
strategy: {
8-
cognito: true,
8+
oauthUrl: 'http://example.com',
99
},
1010
},
1111
server: {

Diff for: sampleData/r4FhirConfigNoGeneric.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const config: FhirConfig = {
55
orgName: 'Organization Name',
66
auth: {
77
strategy: {
8-
cognito: true,
8+
oauthUrl: 'http://example.com',
99
},
1010
},
1111
server: {

Diff for: sampleData/r4FhirConfigWithExclusions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const config: FhirConfig = {
55
orgName: 'Organization Name',
66
auth: {
77
strategy: {
8-
cognito: true,
8+
oauthUrl: 'http://example.com',
99
},
1010
},
1111
server: {

Diff for: scripts/init-auth.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
ClientId=sys.argv[1]
1818
)
1919

20-
sessionid = response['AuthenticationResult']['IdToken']
20+
sessionid = response['AuthenticationResult']['AccessToken']
2121
print(sessionid)

Diff for: scripts/provision-user.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@
5555
}
5656
)
5757

58+
response = client.admin_add_user_to_group(
59+
UserPoolId=sys.argv[1],
60+
Username='workshopuser',
61+
GroupName='practitioner'
62+
)
63+
5864
response = client.initiate_auth(
5965
AuthFlow='USER_PASSWORD_AUTH',
6066
AuthParameters={
@@ -65,5 +71,5 @@
6571
ClientId=sys.argv[2]
6672
)
6773

68-
sessionid = response['AuthenticationResult']['IdToken']
74+
sessionid = response['AuthenticationResult']['AccessToken']
6975
print(sessionid)

Diff for: serverless.yaml

+13-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ custom:
44
resourceTableName: 'resource-${self:custom.stage}'
55
stage: ${opt:stage, self:provider.stage}
66
region: ${opt:region, self:provider.region}
7+
oauthCallback: ${opt:oauthCallback, 'http://localhost'}
8+
oauthRedirect: ${opt:oauthRedirect, 'http://localhost'}
79
config: ${file(serverless_config.json)}
810

911
provider:
@@ -24,6 +26,7 @@ provider:
2426
FHIR_BINARY_BUCKET:
2527
Ref: FHIRBinaryBucket
2628
ELASTICSEARCH_DOMAIN_ENDPOINT: !Join ['', ['https://', !GetAtt ElasticSearchDomain.DomainEndpoint]]
29+
OAUTH2_DOMAIN_ENDPOINT: !Join ['', ['https://', !Ref UserPoolDomain, '.auth.${self:custom.region}.amazoncognito.com/oauth2']]
2730
apiKeys:
2831
- name: developer-key
2932
description: Key for developer to access the FHIR Api
@@ -84,6 +87,10 @@ functions:
8487
type: COGNITO_USER_POOLS
8588
authorizerId:
8689
Ref: ApiGatewayAuthorizer
90+
scopes: # must have both scopes
91+
- 'openid'
92+
- 'profile'
93+
- 'aws.cognito.signin.user.admin'
8794
method: ANY
8895
path: /
8996
private: true
@@ -92,6 +99,10 @@ functions:
9299
type: COGNITO_USER_POOLS
93100
authorizerId:
94101
Ref: ApiGatewayAuthorizer
102+
scopes: # must have both scopes
103+
- 'openid'
104+
- 'profile'
105+
- 'aws.cognito.signin.user.admin'
95106
method: ANY
96107
path: '{proxy+}'
97108
private: true
@@ -277,11 +288,11 @@ resources:
277288
ElasticSearchDomainKibanaEndpoint:
278289
Condition: isDev
279290
Description: ElasticSearch Kibana endpoint
280-
Value: !Join ['', ['https://', !GetAtt ElasticSearchDomain.DomainEndpoint, '/_plugin/kibana']]
291+
Value: !Join ['', ['https://', !GetAtt ElasticSearchDomain.DomainEndpoint, '/_plugin/kibana']]
281292
ElasticSearchKibanaUserPoolId:
282293
Condition: isDev
283294
Description: User pool id for the provisioning ES Kibana users.
284-
Value: !Ref KibanaUserPool
295+
Value: !Ref KibanaUserPool
285296
ElasticSearchKibanaUserPoolAppClientId:
286297
Condition: isDev
287298
Description: App client id for the provisioning ES Kibana users.

Diff for: src/FHIRServerConfig.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { R4_RESOURCE, R3_RESOURCE, VERSION, INTERACTION } from './constants';
22

33
export interface Strategy {
4-
cognito?: boolean;
4+
oauthUrl?: string;
55
}
66

77
export interface Auth {

Diff for: src/app.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import S3ObjectStorageService from './objectStorageService/s3ObjectStorageServic
1111
import ElasticSearchService from './searchService/elasticSearchService';
1212
import BundleResourceRoute from './routes/bundleResourceRoute';
1313
import { DynamoDb } from './dataServices/ddb/dynamoDb';
14+
import RBACHandler from './authorization/RBACHandler';
15+
import RBACRules from './authorization/RBACRules';
16+
import { cleanAuthHeader } from './common/utilities';
1417

1518
// TODO handle multi versions in one server
1619
const configHandler: ConfigHandler = new ConfigHandler(fhirConfig);
@@ -21,6 +24,7 @@ const genericInteractions: INTERACTION[] = configHandler.getGenericInteractions(
2124
const searchParams = configHandler.getSearchParam();
2225

2326
const dynamoDbDataService = new DynamoDbDataService(DynamoDb);
27+
const authService = new RBACHandler(RBACRules);
2428

2529
const genericResourceHandler: ResourceHandler = new ResourceHandler(
2630
dynamoDbDataService,
@@ -45,6 +49,24 @@ const app = express();
4549
app.use(express.urlencoded({ extended: true }));
4650
app.use(express.json());
4751

52+
// AuthZ
53+
app.use(async (req: express.Request, res: express.Response, next) => {
54+
try {
55+
const isAllowed: boolean = authService.isAuthorized(
56+
cleanAuthHeader(req.headers.authorization),
57+
req.method,
58+
req.path,
59+
);
60+
if (isAllowed) {
61+
next();
62+
} else {
63+
res.status(403).json({ message: 'Forbidden' });
64+
}
65+
} catch (e) {
66+
res.status(403).json({ message: `Forbidden. ${e.message}` });
67+
}
68+
});
69+
4870
// Capability Statement
4971
app.use('/metadata', metadataRoute.router);
5072

@@ -65,7 +87,7 @@ genericFhirResources.forEach((resourceType: string) => {
6587
});
6688

6789
// We're not using the GenericResourceRoute because Bundle '/' path only support POST
68-
const bundleResourceRoute = new BundleResourceRoute(dynamoDbDataService, fhirVersion, serverUrl);
90+
const bundleResourceRoute = new BundleResourceRoute(dynamoDbDataService, authService, fhirVersion, serverUrl);
6991
app.use('/', bundleResourceRoute.router);
7092

7193
// Handle errors

Diff for: src/authorization/RBACConfig.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { INTERACTION, R4_RESOURCE } from '../constants';
2+
3+
export interface RBACConfig {
4+
version: number;
5+
groupRules: GroupRule;
6+
}
7+
8+
export interface GroupRule {
9+
[groupName: string]: Rule;
10+
}
11+
export interface Rule {
12+
interactions: INTERACTION[];
13+
resources: R4_RESOURCE[];
14+
}

0 commit comments

Comments
 (0)