aws-amplify / amplify-android

The fastest and easiest way to use AWS from your Android app.
https://docs.amplify.aws/lib/q/platform/android/
Apache License 2.0
243 stars 114 forks source link

API Auth (OIDC): Cognito cannot be used as issuer #1535

Open dnys1 opened 2 years ago

dnys1 commented 2 years ago

Before opening, please confirm:

Language and Async Model

Not applicable

Amplify Categories

GraphQL API, DataStore

Gradle script dependencies

```groovy implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.amplifyframework:core:1.28.0' implementation "com.amplifyframework:aws-api:1.28.0" implementation "com.amplifyframework:aws-api-appsync:1.28.0" ```

Environment information

``` # Put output below this line ------------------------------------------------------------ Gradle 6.7 ------------------------------------------------------------ Build time: 2020-10-14 16:13:12 UTC Revision: 312ba9e0f4f8a02d01854d1ed743b79ed996dfd3 Kotlin: 1.3.72 Groovy: 2.5.12 Ant: Apache Ant(TM) version 1.10.8 compiled on May 10 2020 JVM: 11.0.12 (Amazon.com Inc. 11.0.12+7-LTS) OS: Mac OS X 11.6 x86_64 ```

Please include any relevant guides or documentation you're referencing

https://docs.amplify.aws/lib/graphqlapi/authz/q/platform/android/#oidc

Describe the bug

Cognito Identity Tokens cannot be used with owner auth due to logic in core switching the "cognito:username" identity claim for "username".

Cognito ID tokens and Access Tokens have different structures. This logic in core seems to be accommodating changes to the latter; however, as a result, it seems to have also broken the former.

From Cognito docs:

ID Token Payload

{
    "sub": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "aud": "xxxxxxxxxxxxexample",
    "email_verified": true,
    "token_use": "id",
    "auth_time": 1500009400,
    "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example",
    "cognito:username": "janedoe",
    "exp": 1500013000,
    "given_name": "Jane",
    "iat": 1500009400,
    "email": "janedoe@example.com",
    "jti": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "origin_jti": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
}

Access Token Payload

{
  "sub": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "device_key": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "cognito:groups": [
    "admin"
  ],
  "token_use": "access",
  "scope": "aws.cognito.signin.user.admin",
  "auth_time": 1562190524,
  "iss": "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_example",
  "exp": 1562194124,
  "iat": 1562190524,
  "origin_jti": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "jti": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "client_id": "57cbishk4j24pabc1234567890",
  "username": "janedoe@example.com"
}

Reproduction steps (if applicable)

  1. Use schema below
  2. Set primary auth as OIDC
$ amplify update api
? Please select from one of the below mentioned services: GraphQL
? Select from the options below Update auth settings
? Choose the default authorization type for the API OpenID Connect
? Enter a name for the OpenID Connect provider: Cognito
? Enter the OpenID Connect provider domain (Issuer URL): https://cognito-idp.<REGION>.amazonaws.com/<USER_POOL_ID>/
? Enter the Client Id from your OpenID Client Connect application (optional): 
? Enter the number of milliseconds a token is valid after being issued to a user: 3600000
? Enter the number of milliseconds a token is valid after being authenticated: 3600000
  1. Return Cognito ID token using OIDC auth provider in API category

Code Snippet

// Put your code below this line.

Log output

``` E/amplify:aws-datastore: Failure encountered while attempting to start API sync. DataStoreException{message=Error during subscription., cause=ApiAuthException{message=Attempted to subscribe to a model with owner-based authorization without username which was specified (or defaulted to) as the identity claim., cause=null, recoverySuggestion=If you did not specify a custom identityClaim in your schema, make sure you are logged in. If you did, check that the value you specified in your schema is present in the access key.}, recoverySuggestion=Evaluate details.} at com.amplifyframework.datastore.appsync.AppSyncClient.lambda$subscription$3(AppSyncClient.java:328) at com.amplifyframework.datastore.appsync.-$$Lambda$AppSyncClient$797ziDK0io-qXODzROLOA77stS8.accept(Unknown Source:4) at com.amplifyframework.api.aws.AWSApiPlugin.subscribe(AWSApiPlugin.java:317) at com.amplifyframework.api.aws.AWSApiPlugin.subscribe(AWSApiPlugin.java:288) at com.amplifyframework.api.ApiCategory.subscribe(ApiCategory.java:91) at com.amplifyframework.datastore.appsync.AppSyncClient.subscription(AppSyncClient.java:332) at com.amplifyframework.datastore.appsync.AppSyncClient.onUpdate(AppSyncClient.java:272) at com.amplifyframework.datastore.syncengine.-$$Lambda$r7L8lscweM53-6nW0zECJRGgjT0.subscribe(Unknown Source:7) at com.amplifyframework.datastore.syncengine.SubscriptionProcessor.lambda$subscriptionObservable$6$SubscriptionProcessor(SubscriptionProcessor.java:187) at com.amplifyframework.datastore.syncengine.-$$Lambda$SubscriptionProcessor$w6tohapLGUGmW4mOmsvNOno7GVE.subscribe(Unknown Source:11) at io.reactivex.rxjava3.internal.operators.observable.ObservableCreate.subscribeActual(ObservableCreate.java:40) at io.reactivex.rxjava3.core.Observable.subscribe(Observable.java:13099) at io.reactivex.rxjava3.internal.operators.observable.ObservableDoOnEach.subscribeActual(ObservableDoOnEach.java:42) at io.reactivex.rxjava3.core.Observable.subscribe(Observable.java:13099) at io.reactivex.rxjava3.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96) at io.reactivex.rxjava3.core.Scheduler$DisposeTask.run(Scheduler.java:614) at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:65) at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:56) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) at java.lang.Thread.run(Thread.java:923) Caused by: ApiAuthException{message=Attempted to subscribe to a model with owner-based authorization without username which was specified (or defaulted to) as the identity claim., cause=null, recoverySuggestion=If you did not specify a custom identityClaim in your schema, make sure you are logged in. If you did, check that the value you specified in your schema is present in the access key.} at com.amplifyframework.api.aws.auth.AuthRuleRequestDecorator.getIdentityValue(AuthRuleRequestDecorator.java:157) at com.amplifyframework.api.aws.auth.AuthRuleRequestDecorator.decorate(AuthRuleRequestDecorator.java:123) at com.amplifyframework.api.aws.AWSApiPlugin.buildSubscriptionOperation(AWSApiPlugin.java:628) at com.amplifyframework.api.aws.AWSApiPlugin.subscribe(AWSApiPlugin.java:308) at com.amplifyframework.api.aws.AWSApiPlugin.subscribe(AWSApiPlugin.java:288)  at com.amplifyframework.api.ApiCategory.subscribe(ApiCategory.java:91)  at com.amplifyframework.datastore.appsync.AppSyncClient.subscription(AppSyncClient.java:332)  at com.amplifyframework.datastore.appsync.AppSyncClient.onUpdate(AppSyncClient.java:272)  at com.amplifyframework.datastore.syncengine.-$$Lambda$r7L8lscweM53-6nW0zECJRGgjT0.subscribe(Unknown Source:7)  at com.amplifyframework.datastore.syncengine.SubscriptionProcessor.lambda$subscriptionObservable$6$SubscriptionProcessor(SubscriptionProcessor.java:187)  at com.amplifyframework.datastore.syncengine.-$$Lambda$SubscriptionProcessor$w6tohapLGUGmW4mOmsvNOno7GVE.subscribe(Unknown Source:11)  at io.reactivex.rxjava3.internal.operators.observable.ObservableCreate.subscribeActual(ObservableCreate.java:40)  at io.reactivex.rxjava3.core.Observable.subscribe(Observable.java:13099)  at io.reactivex.rxjava3.internal.operators.observable.ObservableDoOnEach.subscribeActual(ObservableDoOnEach.java:42)  at io.reactivex.rxjava3.core.Observable.subscribe(Observable.java:13099)  at io.reactivex.rxjava3.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96)  at io.reactivex.rxjava3.core.Scheduler$DisposeTask.run(Scheduler.java:614)  at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:65)  at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:56)  at java.util.concurrent.FutureTask.run(FutureTask.java:266)  at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)  at java.lang.Thread.run(Thread.java:923)  W/amplify:aws-datastore: API sync failed - transitioning to LOCAL_ONLY. ```

amplifyconfiguration.json

const amplifyconfig = ''' {
    "UserAgent": "aws-amplify-cli/2.0",
    "Version": "1.0",
    "api": {
        "plugins": {
            "awsAPIPlugin": {
                "dsregression": {
                    "endpointType": "GraphQL",
                    "endpoint": "https://zm3nltvjbjhotbjwze7ibdvd7a.appsync-api.us-west-2.amazonaws.com/graphql",
                    "region": "us-west-2",
                    "authorizationType": "OPENID_CONNECT"
                }
            }
        }
    },
    "auth": {
        "plugins": {
            "awsCognitoAuthPlugin": {
                "UserAgent": "aws-amplify-cli/0.1.0",
                "Version": "0.1.0",
                "IdentityManager": {
                    "Default": {}
                },
                "AppSync": {
                    "Default": {
                        "ApiUrl": "https://zm3nltvjbjhotbjwze7ibdvd7a.appsync-api.us-west-2.amazonaws.com/graphql",
                        "Region": "us-west-2",
                        "AuthMode": "OPENID_CONNECT",
                        "ClientDatabasePrefix": "dsregression_OPENID_CONNECT"
                    }
                },
                "CredentialsProvider": {
                    "CognitoIdentity": {
                        "Default": {
                            "PoolId": "us-west-2:83f3a331-1419-40e2-a673-86dccd15f34f",
                            "Region": "us-west-2"
                        }
                    }
                },
                "CognitoUserPool": {
                    "Default": {
                        "PoolId": "us-west-2_GLlRnjYxU",
                        "AppClientId": "1dprmfs3tuonpn2tedcpd3hcg7",
                        "Region": "us-west-2"
                    }
                },
                "Auth": {
                    "Default": {
                        "authenticationFlowType": "USER_SRP_AUTH",
                        "loginMechanisms": [
                            "PREFERRED_USERNAME"
                        ],
                        "signupAttributes": [
                            "EMAIL"
                        ],
                        "passwordProtectionSettings": {
                            "passwordPolicyMinLength": 8,
                            "passwordPolicyCharacters": []
                        },
                        "mfaConfiguration": "OFF",
                        "mfaTypes": [
                            "SMS"
                        ],
                        "verificationMechanisms": [
                            "EMAIL"
                        ]
                    }
                }
            }
        }
    }
}''';

GraphQL Schema

```graphql type Todo @model @auth(rules: [ { allow: owner, provider: oidc, identityClaim: "cognito:username" } ]) { id: ID! name: String! description: String } ```

Additional information and screenshots

No response

lawmicha commented 1 year ago

To allow backwards compat with older clients that were incorrectly generating "cognito:username", we can explicitly check for this case:

  1. If identityClaim is "cognito:username" and provider is userPool, return "username"
  2. Otherwise, return identityClaim.

In the default logic (2), should solve this issue, since the identityClaim is "cognito:username" and provider is ODIC should fall into this case.

Swift issue and proposed code change: https://github.com/aws-amplify/amplify-swift/issues/1467#issuecomment-1155653764