| author | date |
|---|---|
F. Javier L. Campa |
dd MMMM YYYY |
Avilés 🇪🇸 1981 🦖
- Universidad de Oviedo (MSc. Thesis Universitet i Oslo)
- .NET developer, architect, team manager, solutions architect
- Infrastructure and development in AWS, mainly backend 🐍 some frontend ⚛️
- Computers 💻🪛
- Cars 🏎
- Tennis 🎾
- Golf from Saturday? 🤓🏌
2011 Provisioning API for Visma (SOAP, .NET 3.5, on-prem IIS)
2014 SCIM API for Visma identities (REST, .NET 3.5, AWS BeanStalk)
2016 Admin API for Visma Connect IdP (REST, .NET Core, AWS ECS)
2018 Serverless Speech API for Visma Serverless course (REST, .NET Core, AWS Lambda)
2020 API Maturity Assessment for Visma SDTs (Software Delivery Teams)
2021 Measurements API (IoT) for TAG Sensors (REST, .NET, AWS Lambda)
2022 Developing REST APIs using API CodeFlow (Webstep Fagdag 2022 foredrag)
- Quick overview
- API technologies
- Authentication mechanisms
- Customer case: Aidee Health
- GraphQL
- schema
- resolvers
- mapping templates
- Authentication: OpenID Connect (HelseID)
- Authorization
- Advanced GraphQL features
- Delegation, Transforms, Merging/Stitching
- Cognito + HelseID integration
- REST
- SOAP
- GraphQL
- gRPC
- Verb + RESOURCE + [CHANGE] = HTTP_STATUS + [STATE]
- OpenAPI/Swagger
- De facto standard, easy to understand
- Service oriented, complex types and function calls
- WSDL
- Been around a while!
- Minimize round trips to the API (get what you need, not more, not less)
- GraphQL Schema
- Easy to unify heterogenous backends through a common schema/gateway
- Serialization-Deserialization into raw-byte
- Protocol buffers
- Resource efficient!
Next generation continuous blood pressure sampling devices
Web application to register and display patient and equipment reference data
- Patients/users
- Doctors
- Data analysts
- Administrators
- Authentication through national health identity providers
- Strict data isolation
~~~graph-easy --as=boxart
[Web] -> [API] -> [DB]
~~~
type Patient {
id: ID!
name: String!
age: Int!
}input PatientInput {
name: String!
age: Int!
}type Query {
getPatient(patientid: ID!): Patient
}type Mutation {
createPatient(input: PatientInput!): Patient
}type Subscription {
onCreatedPatient(patientid: ID): Patient
}{
"query": "query GetPatient($patientid: ID!){ getPatient(patientid: $patientid) { name }",
"variables": {
"patientid": "patient1234"
}
}{
"data": {
"getPatient": {
"name": "Pedro"
}
}
}{
"query": "mutation CreatePatient($input: PatientInput!){ createPatient(input: $input) { name age } }",
"variables": {
"input": {
"name": "Pedro",
"age": 42
}
}
}{
"data": {
"createPatient": {
"name": "Pedro",
"age": 42
}
}
}~~~graph-easy --as=boxart
[Hospital]- has-many ->[Patient]
~~~
type Hospital {
id: ID!
address: String!
name: String!
patients: [Patient]
}type Patient {
id: ID!
age: Int!
name: String!
gender: String!
hospital: Hospital
}"Retrieve hospital, age and gender for all patients"
GET /hospital?fields=id,name
foreach(h in hospital.id)
GET /hospital/h/patient?fields=name,gender
"Retrieve hospital, age and gender for all patients"
GET /hospital?fields=id,name
foreach(h in hospital.id)
GET /hospital/h/patient?fields=name,gender
query listPatients {
age
gender
hospital { name }
}
~~~graph-easy --as=boxart
[Hospital]- has-many ->[Patient]- has-many ->[Measurements]
~~~
type Patient {
id: ID!
age: Int!
name: String!
gender: String!
hospital: Hospital
measurements: [Measurement]
}type Measurement {
timestamp: Int!
bloodPressure: Float!
pulse: Int!
}"Retrieve age, gender and measurements for all patients"
GET /hospital?fields=id
foreach(h in hospital.id)
GET /hospital/h/patient?fields=id,age,gender
foreach(p in patient.id)
GET /hospital/h1/patient/p/measurements
"Retrieve age, gender and measurements for all patients"
GET /hospital?fields=id
foreach(h in hospital.id)
GET /hospital/h/patient?fields=id,age,gender
foreach(p in patient.id)
GET /hospital/h1/patient/p/measurements
query listHospital {
patients {
age
gender
measurements { timestamp bloodPressure pulse }
}
}
"How is data fetched from the data source?"
type Query {
getHospital(hospitalId:ID!): Hospital # How do we fetch data?
}"How is data fetched from the data source?"
type Query {
getHospital(hospitalId:ID!): Hospital # How do we fetch data?
}type Hospital {
id: ID!
name: String!
patients: [Patient] # How do we populate this field?
}"How is data fetched from the data source?"
- type
- field
- data source
- request template
- response template
type Query {
getHospital(hospitalId:ID!): Hospital
}Resolver:
{
"type": "Query",
"field": "getHospital",
"datasource": "HospitalsTable in DynamoDB",
"request mapping template": "getHospital_request.vtl",
"response mapping template": "getHospital_response.vtl"
}{
"version": "2017-02-28",
"operation": "GetItem",
"key": {
"hospitalId": $util.dynamodb.toDynamoDBJson($context.args.hospitalId)
}
}$utils.toJson($context.result)type Hospital {
id: ID!
name: String!
patients: [Patient] # How to populate this field?
}Resolver:
{
"type": "Hospital",
"field": "patients",
"datasource": "PatientsTable in DynamoDB",
"request mapping template": "hospitalPatients_request.vtl",
"response mapping template": "hospitalPatients_response.vtl"
}{
"version": "2017-02-28",
"operation": "Query",
"query": {
"expression": "hospitalId = :hospitalId",
"expressionValues": {
":hospitalId": {
"S": $util.toJson($context.source.hospitalId)
}
}
}
}$utils.toJson($context.result) # list of itemsquery getHospital("h1") {
name
}query getHospital("h1") {
name
}~~~graph-easy --as=boxart
[Request]- 1. getHospital ->[Query Resolver]- 2. req. mapping template ->[Data source]
[Data source]- 3. resp. mapping template ->[Query Resolver]
[Query Resolver]->[Response]
~~~
query getHospital("h1") {
name
patients { name }
}query getHospital("h1") {
name
patients { name }
}~~~graph-easy --as=boxart
[Request]- patients ->[ Field Resolver ]->[Response]
[Request]- getHospital ->[ Query Resolver ] -> [Response]
~~~
- Rate limiting and depth limiting
- Pagination (nextToken)
- Other data sources, including Lambda
- Pipeline resolvers
- Static secret shared between client and server
- Must be rotated
- Can leak, be stolen, etc.
- Both client and server verify certificates mutually
- Added operational complexity: certificate issuing, revocation lists, renewals, etc.
- Best suited for machine-to-machine
- OpenID Connect extends OAuth 2.0 with user authentication and SSO
- OAuth 2.0 controls authorization
- Separation of authentication and authorization, client (browser, mobile app) and user (person)
- Multiple authentication flows to fit different requirements
- Identity Provider (IdP): a third party verifying the users identity
- Normally through username+password, 2FA, etc.
- Identity Provider (IdP): a third party verifying the users identity
- Normally through username+password, 2FA, etc.
- IdP authenticates user and provides a token to the client (browser)
- identity and claims
- cryptographically protected against tampering (cannot be altered)
- short life (quick expiration)
~~~graph-easy --as=boxart
[Web] <-> [Cognito] <-> [HelseID]
[Web] <-> [API] <-> [Cognito]
~~~
- User directory
- OpenID Connect identity/service provider
- Integrates with multitude of AWS services
- Supports external login with third-party SAML and OpenID Connect identity providers
- National authentication service for the healthcare sector in Norway
- OpenID Connect identity provider
~~~graph-easy --as=boxart
[Web] - 1 -> [Cognito] - 2 -> [HelseID]
[HelseID] <-> [BankID]{ border: 1px dotted black; }
~~~
- App redirects to Cognito on Login
- Cognito checks cookie, if not present redirects to HelseID
~~~graph-easy --as=boxart
[Web] -> [Cognito] -> [HelseID]
[HelseID] - 3 -> [BankID]{ border: 1px dotted black; }
[HelseID] <- 4 -> [BankID]{ border: 1px dotted black; }
~~~
- HelseID checks cookie, if not present redirects to third-party IdP (BankID, IDPorten)
- Third-party IdP
- Beyond HelseID is a black box for us
~~~graph-easy --as=boxart
[Web] -> [Cognito] -> [HelseID]
[HelseID] <-> [BankID]{ border: 1px dotted black; }
[HelseID] - 5 code -> [Cognito]
[Cognito] - 6 id_token -> [HelseID]
~~~
- HelseID redirects to Cognito with a code in the URL (front-channel)
- Cognito uses code to retrieve HelseID id_token through an API call (back-channel)
~~~graph-easy --as=boxart
[Web] - 8 id_token -> [Cognito] -> [HelseID]
[HelseID] <-> [BankID]{ border: 1px dotted black; }
[Cognito] - 7 code -> [Web]
~~~
- Cognito redirects to Web with a code in the URL
- Web uses code to retrieve Cognito id_token through an API call
- User is authenticated with Cognito and the client (browser) has a Cognito token
{
"sub": "c1a1d232-df61-4157-b7d2-38881191e55c",
"cognito:groups": [
"hp",
],
"custom:idp.hp_number": "43218765",
"exp": 1681462824,
"given_name": "Aemon",
"aud": "HealthApp",
"name": "Aemon Targaryen",
"family_name": "Targaryen",
[...]
}- Client validates token
- sends it when issuing API calls
- API validates token
- valid audience
- online or offline
- API authorizes call
- how?
"Who can do what?"
- health personnel
- patients
- admin
"Who can do what?"
- health personnel
- patients
- admin
- @aws_auth
"Who can do what?"
- health personnel
- patients
- admin
- @aws_auth
Velicity Template Language or JavaScript
- Request
- Cognito tokens claims
- Utils
Health services - strict data access requirements
- "Doctors can access patient data"
- "Patients can only access their own data"
- "Administrators cannot access patients sensitive data"
- "Doctors can access patient data"
- "Doctors can access patient data"
type Query {
getPatient(patientId:ID!): Patient @aws_auth(cognito_groups:["hp"])
}- "Patients can only access their own data"
- "Patients can only access their own data"
type Query {
getPatient(patientId:ID!): Patient @aws_auth(cognito_groups:["hp","patient"])
}- "Patients can only access their own data"
type Query {
getPatient(patientId:ID!): Patient @aws_auth(cognito_groups:["hp","patient"])
}type Query {
getPatient(patientId:ID!): Patient @aws_auth(cognito_groups:["hp"])
getPatient(): Patient @aws_auth(cognito_groups:["patient"])
}- Patients are not allowed to query by patientId
- Circumvented IDOR (insecure direct object reference)
- "sub" in JWT token used instead (see next), which cannot be altered
- "Patients can only access their own data"
#foreach($group in $context.identity.claims.get("cognito:groups"))
#if($group == "patient")
#set($isPatient = true)
#break
#end
#if($isPatient)
{
"version": "2017-02-28",
"operation": "GetItem",
"key": {
"patientid": $util.dynamodb.toDynamoDBJson($context.identity.sub)
}
}
#else
$utils.unauthorized()
#end- "Administrators cannot access patients sensitive data"
type Query {
getPatient(patientId:ID!): Patient @aws_auth(cognito_groups:["hp","admin"])
getPatient(): Patient @aws_auth(cognito_groups:["patient"])
}
type Patient {
id: ID!
age: Int!
name: String!
gender: String!
hospital: Hospital
measurements: [Measurement] @aws_auth(cognito_groups:["hp","patient"])
}"How do we know who is a doctor, an administrator, a patient, or all at once!?"
- admin group is assigned manually to certain individuals
"How do we know who is a doctor, an administrator, a patient, or all at once!?"
- admin group is assigned manually to certain individuals
- hp? Sync with HelseID health personel register?
"How do we know who is a doctor, an administrator, a patient, or all at once!?"
- admin group is assigned manually to certain individuals
- hp? Sync with HelseID health personel register?
- Based on the existence of custom:idp.hp_number claim
- Assignment lasts the duration of the session
https://github.com/xavicampa/appsync-workshop
- Questions?
- Feedback?
@javier
fjcampalus (barely use it 🙈)
@fjcampalus (I don't use it 🤦)