AWS Amplify Notes
Last Updated: March 10, 2022
Using OIDC (Azure AD) with Amplify / AppSync
If your application already has an existing identity provider, such as Azure AD, Auth0, or your own, you can omit Cognito User Pools entirely from your Amplify / AppSync application. This can provide cost savings if you just authenticate with SAML through Cognito. At the time of writing the price was $0.015 per user, which is absurd if you are only authenticating through your IdP on Cognito.
"For users who sign in through SAML or OIDC federation, the price for MAUs above the 50 MAU free tier is $0.015."
CLI Parameters
It will ask for these parameters if you run amplify add api
and choose OIDC for the authentication option.
You can also find the client ID by looking at the aud
claim on your token. There is a different issuer URL if you are using the v2.0 token endpoint. Refer to your app registration for the issuer URL.
If you are not using Amplify, enter this in the AppSync settings.
"defaultAuthType": {
"mode": "OPENID_CONNECT",
"openIDProviderName": "azure",
"openIDIssuerURL": "https://sts.windows.net/<tenant_id>/",
"openIDClientID": "<client_id>",
"openIDAuthTTL": "0",
"openIDIatTTL": "0"
}
Schema
In the schema, you can define the provider as oidc
and set the groupClaim
to the claim in your JWT that contains what groups the user is a member of. In this case I have two Azure AD App Roles defined as Administrator and Technician.
type Task
@model
@auth(
rules: [
{allow: private, provider: iam, operations: [create, update, read, delete]},
{allow: groups, provider: oidc, groupClaim: "roles", groups: ["Administrator"], operations: [create, update, read, delete]},
{allow: groups, provider: oidc, groupClaim: "roles", groups: ["Technician"], operations: [read]}
]
) {
id: ID!
name: String!
}
If you want to use the user name (owner) for auth, you can use the oid
claim. The oid
claim is guaranteed to be unique per user but it is in a UUID format. You can also add the upn
claim to your token in the app registration settings. Another option is to use the unique_name
or email
claim but they may not be unique. Also it is possible to have a user without an email in Azure AD.
{ allow: owner, provider: oidc, identityClaim: "upn" }
You can also allow all OIDC authenticated users to read:
{ allow: owner, provider: oidc, operations: [read] }
Client Side Code (Angular using the MSAL Library)
After you've subscribed to the authentication complete event, we'll initialize the DataStore. If you're not using the DataStore and just the API library, you can do the same thing but omit the datastore lines.
The trick to get the Amplify library to recognize that you are logged in is to set the federatedInfo
cache entry with your access token.
In order for the graphql calls to use your token obtained by MSAL, you'll have to override the graphql_headers
option. If you only set the federatedInfo
property then your app with stop working after the token expires (1 hour) and not grab a new one.
I am waiting for the dataStoreReady
property to be true
in order for the app to display anything to the user but this might not be needed depending on your situation.
export class AppComponent implements OnInit, OnDestroy {
private readonly destroying$ = new Subject<void>();
private dataStoreListener;
dataStoreReady = false;
constructor(private authService: MsalService,
private msalBroadcastService: MsalBroadcastService,
private updateService: AppUpdateService) {
}
ngOnInit(): void {
this.authService.handleRedirectObservable().subscribe();
// Watch for the auth complete event
this.msalBroadcastService.inProgress$
.pipe(
filter((status: InteractionStatus) => status === InteractionStatus.None),
takeUntil(this.destroying$)
)
.subscribe(async () => {
this.checkAndSetActiveAccount();
await this.initializeDatastore();
});
}
ngOnDestroy(): void {
this.destroying$.next();
this.destroying$.complete();
this.dataStoreListener();
}
checkAndSetActiveAccount(): void {
const activeAccount = this.authService.instance.getActiveAccount();
if (!activeAccount && this.authService.instance.getAllAccounts().length > 0) {
const accounts = this.authService.instance.getAllAccounts();
this.authService.instance.setActiveAccount(accounts[0]);
}
}
async initializeDatastore(): Promise<void> {
const activeAccount = this.authService.instance.getActiveAccount();
Cache.removeItem('federatedInfo');
if (activeAccount) {
const token = await this.acquireToken();
Cache.setItem('federatedInfo', {token: token.accessToken});
API.configure({
'graphql_headers': async () => {
const token = await this.acquireToken();
return {
Authorization: token.accessToken
};
},
});
await DataStore.start();
this.dataStoreListener = Hub.listen('datastore', async hubData => {
const {event, data} = hubData.payload;
if (event === 'ready') {
this.dataStoreReady = true;
}
});
}
}
async acquireToken(): Promise<AuthenticationResult> {
return this.authService.acquireTokenSilent({
scopes: [environment.apiScope],
authority: environment.authority,
}).toPromise();
}
Using CodeArtifact packages in Lambda Layer managed by Amplify
The following is a guide on how to use CodeArtifact packages in your lambda layer (or lambda). This can also be used to use CodeArtifacts in a general Pipfile
amplify.yml
Add this to your backend prebuild command
export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token --domain <domain> --domain-owner <owner_id> --query authorizationToken --output text)
Pipfile
[[source]]
name = "pypi"
url = "https://aws:$CODEARTIFACT_AUTH_TOKEN@<domain>.d.codeartifact.us-west-2.amazonaws.com/pypi/<repo_name>/simple"
verify_ssl = true
[dev-packages]
[packages]
<package_name> = '==0.0.1'
[requires]
python_version = "3.9"
Publishing package
aws codeartifact login --tool twine --repository <repo> --domain <domain> --domain-owner <id>
python setup.py sdist bdist_wheel
twine upload --repository codeartifact dist/*
Reducing Lambda sizes
Amplify builds Python Lambda functions and automatically installs pipenv and setuptools. This usually is not needed if you are using Lambda layers and creates an unnecessary ~2MB lambda function.
You can trick Amplify to not build your function by changing the runtime to NodeJS.
Open up the amplify.state
file in your function directory and change the pluginId
and functionRuntime
to nodejs
.
{
"pluginId": "amplify-nodejs-function-runtime-provider",
"functionRuntime": "nodejs",
"useLegacyBuild": false,
"defaultEditorFile": "src/index.py"
}