Skip to content

Latest commit

 

History

History
executable file
·
721 lines (710 loc) · 15.8 KB

File metadata and controls

executable file
·
721 lines (710 loc) · 15.8 KB
author date
F. Javier L. Campa
dd MMMM YYYY

Yo

Avilés 🇪🇸 1981 🦖

MSc. Computer Science

  • Universidad de Oviedo (MSc. Thesis Universitet i Oslo)

2008 - 2021 Visma

  • .NET developer, architect, team manager, solutions architect

2021 - now Webstep

  • Infrastructure and development in AWS, mainly backend 🐍 some frontend ⚛️

Outside of work

  • Computers 💻🪛
  • Cars 🏎
  • Tennis 🎾
  • Golf from Saturday? 🤓🏌

API journey

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)

2022 Studies API for Aidee Health AS (GraphQL, Python, AWS AppSync)

Agenda

  • Quick overview
    • API technologies
    • Authentication mechanisms
  • Customer case: Aidee Health
  • GraphQL
    • schema
    • resolvers
    • mapping templates
  • Authentication: OpenID Connect (HelseID)
  • Authorization

What we won't cover

  • Advanced GraphQL features
    • Delegation, Transforms, Merging/Stitching
  • Cognito + HelseID integration

Overview of API technologies

  • REST
  • SOAP
  • GraphQL
  • gRPC
Skip? Jump 3!

REST

  • Verb + RESOURCE + [CHANGE] = HTTP_STATUS + [STATE]
  • OpenAPI/Swagger
  • De facto standard, easy to understand

SOAP

  • Service oriented, complex types and function calls
  • WSDL
  • Been around a while!

GraphQL

  • 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

gRPC

  • Serialization-Deserialization into raw-byte
  • Protocol buffers
  • Resource efficient!

Aidee Health AS (aidee.io)

Next generation continuous blood pressure sampling devices

Project

Web application to register and display patient and equipment reference data

Actors

  • Patients/users
  • Doctors
  • Data analysts
  • Administrators

Requirements

  • Authentication through national health identity providers
  • Strict data isolation

Architecture

~~~graph-easy --as=boxart
[Web] -> [API] -> [DB]
~~~

React + AmplifyJS/UI (TypeScript) Amazon CloudFront (CDN)

AWS AppSync (GraphQL)

Amazon DynamoDB (NoSQL)


GraphQL schema

Types

type Patient {
    id: ID!
    name: String!
    age: Int!
}

Inputs

input PatientInput {
    name: String!
    age: Int!
}

Queries

type Query {
    getPatient(patientid: ID!): Patient
}

Mutations

type Mutation {
    createPatient(input: PatientInput!): Patient
}

Subscriptions

type Subscription {
    onCreatedPatient(patientid: ID): Patient
}

GraphQL examples

Simple query

{
    "query": "query GetPatient($patientid: ID!){ getPatient(patientid: $patientid) { name }",
    "variables": {
        "patientid": "patient1234"
    }
}

Response

{
    "data": {
        "getPatient": {
            "name": "Pedro"
        }
    }
}

Simple mutation

{
    "query": "mutation CreatePatient($input: PatientInput!){ createPatient(input: $input) { name age } }",
    "variables": {
        "input": {
            "name": "Pedro",
            "age": 42
        }
    }
}

Response

{
    "data": {
        "createPatient": {
            "name": "Pedro",
            "age": 42
        }
    }
}

Query depth

~~~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
}

Imagine the following requirement

"Retrieve hospital, age and gender for all patients"

REST

GET /hospital?fields=id,name
foreach(h in hospital.id)
    GET /hospital/h/patient?fields=name,gender

Imagine the following requirement

"Retrieve hospital, age and gender for all patients"

REST

GET /hospital?fields=id,name
foreach(h in hospital.id)
    GET /hospital/h/patient?fields=name,gender

GraphQL

query listPatients {
    age
    gender
    hospital { name }
}

Query depth (II)

~~~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!
}

Listing requirement for frontend

"Retrieve age, gender and measurements for all patients"

REST

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

Listing requirement for frontend

"Retrieve age, gender and measurements for all patients"

REST

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

GraphQL

query listHospital {
    patients {
        age
        gender
        measurements { timestamp bloodPressure pulse }
    }
}

Resolvers

"How is data fetched from the data source?"

Resolvers

"How is data fetched from the data source?"

Query resolvers

type Query {
    getHospital(hospitalId:ID!): Hospital # How do we fetch data?
}

Resolvers

"How is data fetched from the data source?"

Query resolvers

type Query {
    getHospital(hospitalId:ID!): Hospital # How do we fetch data?
}

Field resolvers

type Hospital {
    id: ID!
    name: String!
    patients: [Patient] # How do we populate this field?
}

Resolvers

"How is data fetched from the data source?"

Defining resolvers (AWS AppSync)

  • type
  • field
  • data source
  • request template
  • response template

Query resolvers

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"
}

getHospital_request.vtl

{
    "version": "2017-02-28",
    "operation": "GetItem",
    "key": {
        "hospitalId": $util.dynamodb.toDynamoDBJson($context.args.hospitalId)
    }
}

getHospital_response.vtl

$utils.toJson($context.result)

Field resolvers

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"
}

hospitalPatients_request.vtl

{
    "version": "2017-02-28",
    "operation": "Query",
    "query": {
        "expression": "hospitalId = :hospitalId",
        "expressionValues": {
            ":hospitalId": {
                "S": $util.toJson($context.source.hospitalId)
            }
        }
    }
}

hospitalPatients_response.vtl

$utils.toJson($context.result)  # list of items

Execution of a query

query getHospital("h1") {
    name
}

Execution of a query

query getHospital("h1") {
    name
}

Flow

~~~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]
~~~

Execution of a query (II)

query getHospital("h1") {
    name
    patients { name }
}

Execution of a query (II)

query getHospital("h1") {
    name
    patients { name }
}

Flow

~~~graph-easy --as=boxart
[Request]- patients ->[ Field Resolver ]->[Response]
[Request]- getHospital ->[ Query Resolver ] -> [Response]
~~~

Resolvers: other topics

  • Rate limiting and depth limiting
  • Pagination (nextToken)
  • Other data sources, including Lambda
  • Pipeline resolvers

Authentication mechanisms

API keys

  • Static secret shared between client and server
  • Must be rotated
  • Can leak, be stolen, etc.

m(utual)TLS

  • Both client and server verify certificates mutually
  • Added operational complexity: certificate issuing, revocation lists, renewals, etc.
  • Best suited for machine-to-machine

OAuth 2.0 and OpenID Connect (OIDC)

  • 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

OAuth 2.0 and OpenID Connect (OIDC)

  • Identity Provider (IdP): a third party verifying the users identity
    • Normally through username+password, 2FA, etc.

OAuth 2.0 and OpenID Connect (OIDC)

  • 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)

Integrating HelseID (OpenID Connect) with AWS services

~~~graph-easy --as=boxart
[Web] <-> [Cognito] <-> [HelseID]
[Web] <-> [API] <-> [Cognito]
~~~

Amazon 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

HelseID

  • National authentication service for the healthcare sector in Norway
  • OpenID Connect identity provider

Code flow

~~~graph-easy --as=boxart
[Web] - 1 -> [Cognito] - 2 -> [HelseID]
[HelseID] <-> [BankID]{ border: 1px dotted black; }
~~~
  1. App redirects to Cognito on Login
  2. Cognito checks cookie, if not present redirects to HelseID

Code flow

~~~graph-easy --as=boxart
[Web] -> [Cognito] -> [HelseID]
[HelseID] - 3 -> [BankID]{ border: 1px dotted black; }
[HelseID] <- 4 -> [BankID]{ border: 1px dotted black; }
~~~
  1. HelseID checks cookie, if not present redirects to third-party IdP (BankID, IDPorten)
  2. Third-party IdP
  • Beyond HelseID is a black box for us

Code flow

~~~graph-easy --as=boxart
[Web] -> [Cognito] -> [HelseID]
[HelseID] <-> [BankID]{ border: 1px dotted black; }
[HelseID] - 5 code -> [Cognito]
[Cognito] - 6 id_token -> [HelseID]
~~~
  1. HelseID redirects to Cognito with a code in the URL (front-channel)
  2. Cognito uses code to retrieve HelseID id_token through an API call (back-channel)

Code flow

~~~graph-easy --as=boxart
[Web] - 8 id_token -> [Cognito] -> [HelseID]
[HelseID] <-> [BankID]{ border: 1px dotted black; }
[Cognito] - 7 code -> [Web]
~~~
  1. Cognito redirects to Web with a code in the URL
  2. 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

Example 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",
  [...]
}

Token validation

  • Client validates token
    • sends it when issuing API calls
  • API validates token
    • valid audience
    • online or offline
  • API authorizes call
    • how?

Authorization

"Who can do what?"

Authorization

"Who can do what?"

Cognito groups

  • health personnel
  • patients
  • admin

Authorization

"Who can do what?"

Cognito groups

  • health personnel
  • patients
  • admin

AWS directives in GraphQL schema

  • @aws_auth

Authorization

"Who can do what?"

Cognito groups

  • health personnel
  • patients
  • admin

AWS directives in GraphQL schema

  • @aws_auth

Custom logic in mapping templates

Velicity Template Language or JavaScript

  • Request
  • Cognito tokens claims
  • Utils

Authorization

Health services - strict data access requirements

  • "Doctors can access patient data"
  • "Patients can only access their own data"
  • "Administrators cannot access patients sensitive data"

Authorization

  • "Doctors can access patient data"

Authorization

  • "Doctors can access patient data"

Schema

type Query {
    getPatient(patientId:ID!): Patient  @aws_auth(cognito_groups:["hp"])
}

Authorization

  • "Patients can only access their own data"

Authorization

  • "Patients can only access their own data"

Schema

type Query {
    getPatient(patientId:ID!): Patient  @aws_auth(cognito_groups:["hp","patient"])
}

Authorization

  • "Patients can only access their own data"

Schema

type Query {
    getPatient(patientId:ID!): Patient  @aws_auth(cognito_groups:["hp","patient"])
}

or even better

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

Authorization

  • "Patients can only access their own data"

Request mapping template: getPatient

#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

Authorization

  • "Administrators cannot access patients sensitive data"

Schema

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"])
}

Group membership

"How do we know who is a doctor, an administrator, a patient, or all at once!?"

Group membership

"How do we know who is a doctor, an administrator, a patient, or all at once!?"

Static assignment

  • admin group is assigned manually to certain individuals

Group membership

"How do we know who is a doctor, an administrator, a patient, or all at once!?"

Static assignment

  • admin group is assigned manually to certain individuals
  • hp? Sync with HelseID health personel register?

Group membership

"How do we know who is a doctor, an administrator, a patient, or all at once!?"

Static assignment

  • admin group is assigned manually to certain individuals
  • hp? Sync with HelseID health personel register?

Assignment at login

  • Based on the existence of custom:idp.hp_number claim
  • Assignment lasts the duration of the session

Hacka litt? 🤓

https://github.com/xavicampa/appsync-workshop

Thank you!

  • Questions?
  • Feedback?

Contact

󰇮 javier.campa@webstep.no

 @javier

 fjcampalus (barely use it 🙈)

 @fjcampalus (I don't use it 🤦)