Skip to main content

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