Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Received an UnauthorizedException when calling GraphQL with Auth0 as OIDC. #13252

Open
3 tasks done
TitusEfferian opened this issue Apr 16, 2024 · 3 comments
Open
3 tasks done
Assignees
Labels
GraphQL Related to GraphQL API issues question General question

Comments

@TitusEfferian
Copy link

TitusEfferian commented Apr 16, 2024

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

GraphQL API

Amplify Version

v6

Amplify Categories

api

Backend

Amplify CLI

Environment information

 System:
    OS: macOS 14.4.1
    CPU: (10) arm64 Apple M2 Pro
    Memory: 997.81 MB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 18.19.1 - ~/.nvm/versions/node/v18.19.1/bin/node
    Yarn: 1.22.19 - ~/.nvm/versions/node/v18.19.1/bin/yarn
    npm: 10.2.4 - ~/.nvm/versions/node/v18.19.1/bin/npm
  Browsers:
    Chrome: 123.0.6312.124
    Chrome Canary: 125.0.6421.0
    Edge: 123.0.2420.97
    Safari: 17.4.1
  npmPackages:
    @auth0/auth0-react: ^2.2.4 => 2.2.4 
    @aws-sdk/client-cognito-identity: ^3.540.0 => 3.554.0 
    @types/react: ^18.2.66 => 18.2.77 
    @types/react-dom: ^18.2.22 => 18.2.25 
    @typescript-eslint/eslint-plugin: ^7.2.0 => 7.6.0 
    @typescript-eslint/parser: ^7.2.0 => 7.6.0 
    @vitejs/plugin-react: ^4.2.1 => 4.2.1 
    aws-amplify: ^6.0.28 => 6.0.28 
    aws-amplify/adapter-core:  undefined ()
    aws-amplify/analytics:  undefined ()
    aws-amplify/analytics/kinesis:  undefined ()
    aws-amplify/analytics/kinesis-firehose:  undefined ()
    aws-amplify/analytics/personalize:  undefined ()
    aws-amplify/analytics/pinpoint:  undefined ()
    aws-amplify/api:  undefined ()
    aws-amplify/api/server:  undefined ()
    aws-amplify/auth:  undefined ()
    aws-amplify/auth/cognito:  undefined ()
    aws-amplify/auth/cognito/server:  undefined ()
    aws-amplify/auth/enable-oauth-listener:  undefined ()
    aws-amplify/auth/server:  undefined ()
    aws-amplify/data:  undefined ()
    aws-amplify/data/server:  undefined ()
    aws-amplify/datastore:  undefined ()
    aws-amplify/in-app-messaging:  undefined ()
    aws-amplify/in-app-messaging/pinpoint:  undefined ()
    aws-amplify/push-notifications:  undefined ()
    aws-amplify/push-notifications/pinpoint:  undefined ()
    aws-amplify/storage:  undefined ()
    aws-amplify/storage/s3:  undefined ()
    aws-amplify/storage/s3/server:  undefined ()
    aws-amplify/storage/server:  undefined ()
    aws-amplify/utils:  undefined ()
    eslint: ^8.57.0 => 8.57.0 
    eslint-plugin-react-hooks: ^4.6.0 => 4.6.0 
    eslint-plugin-react-refresh: ^0.4.6 => 0.4.6 
    react: ^18.2.0 => 18.2.0 
    react-dom: ^18.2.0 => 18.2.0 
    typescript: ^5.2.2 => 5.4.5 
    vite: ^5.2.0 => 5.2.8 
  npmGlobalPackages:
    @aws-amplify/cli: 12.10.1
    corepack: 0.22.0
    npm: 10.2.4
    vercel: 33.6.1
    yarn: 1.22.22


Describe the bug

I am trying to perform some CRUD operations with GraphQL, using Auth0 as the OIDC provider. I have successfully logged in with Auth0, obtained the idToken, passed it to Amplify Auth, and received all the results within fetchAuthSession(). Now, I am planning to hit a GraphQL endpoint, but I encountered an "UnauthorizedException" error.

I have explored all the available open and closed issues in this repository using the filter is:issue is:open graphql auth0, and I didn’t find any duplicates or relevant issues related to my case. I have also searched in the aws-amplify Discord and still haven't found any information, so I decided to open a new issue here.

Expected behavior

GraphQL returns a 200 status code, with expected data

Reproduction steps

  1. Create an Amplify project.
  2. Create an Auth0 project.
  3. Follow these docs.
  4. Create a GraphQL schema using amplify add api.
  5. Call the GraphQL API.

Code Snippet

main.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { Auth0Provider } from "@auth0/auth0-react";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <Auth0Provider
    domain="my_domain.auth0.com"
    clientId="my_client_id"
    authorizationParams={{
      redirect_uri: "http://localhost:5173",
    }}
  >
    <App />
  </Auth0Provider>,
);

App.tsx

import { useAuth0 } from "@auth0/auth0-react";
import "./App.css";
import { Amplify } from "aws-amplify";
import {
  fetchAuthSession,
  CredentialsAndIdentityIdProvider,
  CredentialsAndIdentityId,
  GetCredentialsOptions,
  decodeJWT,
  TokenProvider,
} from "aws-amplify/auth";
import awsconfig from "./amplifyconfiguration.json";

// Note: This example requires installing `@aws-sdk/client-cognito-identity` to obtain Cognito credentials
// npm i @aws-sdk/client-cognito-identity
import { CognitoIdentity } from "@aws-sdk/client-cognito-identity";

import { generateClient } from "aws-amplify/api";
import { listAnotherTodos } from "./graphql/queries";

// You can make use of the sdk to get identityId and credentials
const cognitoidentity = new CognitoIdentity({
  region: awsconfig.aws_cognito_region,
});

// Note: The custom provider class must implement CredentialsAndIdentityIdProvider
class CustomCredentialsProvider implements CredentialsAndIdentityIdProvider {
  // Example class member that holds the login information
  federatedLogin?: {
    domain: string;
    token: string;
  };

  // Custom method to load the federated login information
  loadFederatedLogin(login?: typeof this.federatedLogin) {
    // You may also persist this by caching if needed
    this.federatedLogin = login;
  }

  async getCredentialsAndIdentityId(
    getCredentialsOptions: GetCredentialsOptions,
  ): Promise<CredentialsAndIdentityId | undefined> {
    try {
      // You can add in some validation to check if the token is available before proceeding
      // You can also refresh the token if it's expired before proceeding

      const getIdResult = await cognitoidentity.getId({
        // Get the identityPoolId from config
        IdentityPoolId: awsconfig.aws_cognito_identity_pool_id,
        Logins: { [this.federatedLogin.domain]: this.federatedLogin.token },
      });

      const cognitoCredentialsResult =
        await cognitoidentity.getCredentialsForIdentity({
          IdentityId: getIdResult.IdentityId,
          Logins: { [this.federatedLogin.domain]: this.federatedLogin.token },
        });

      const credentials: CredentialsAndIdentityId = {
        credentials: {
          accessKeyId: cognitoCredentialsResult.Credentials?.AccessKeyId,
          secretAccessKey: cognitoCredentialsResult.Credentials?.SecretKey,
          sessionToken: cognitoCredentialsResult.Credentials?.SessionToken,
          expiration: cognitoCredentialsResult.Credentials?.Expiration,
        },
        identityId: getIdResult.IdentityId,
      };
      return credentials;
    } catch (e) {
      console.log("Error getting credentials: ", e);
    }
  }
  // Implement this to clear any cached credentials and identityId. This can be called when signing out of the federation service.
  clearCredentialsAndIdentityId(): void {}
}

// Create an instance of your custom provider
const customCredentialsProvider = new CustomCredentialsProvider();

function App() {
  const { loginWithRedirect, getIdTokenClaims, logout, isLoading } = useAuth0();

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <>
      <button
        onClick={() => {
          loginWithRedirect();
        }}
      >
        login redirect
      </button>
      <button
        // get the token and pass it to amplify
        onClick={async () => {
          try {
            const token = await getIdTokenClaims();
            const myTokenProvider: TokenProvider = {
              async getTokens() {
                return {
                  accessToken: decodeJWT(token?.__raw),
                  idToken: token?.__raw || "",
                };
              },
            };
            Amplify.configure(
              {
                ...awsconfig,
              },
              {
                Auth: {
                  credentialsProvider: customCredentialsProvider,
                  tokenProvider: myTokenProvider,
                },
              },
            );
            await customCredentialsProvider.loadFederatedLogin({
              domain: "my_domain.auth0.com",
              token: token?.__raw || "",
            });
            const fetchSessionResult = await fetchAuthSession(); // will return the credentials
            console.log("fetchSessionResult: ", fetchSessionResult);
          } catch (err) {
            console.log(err);
          }
        }}
      >
        login
      </button>
      <button
        onClick={async () => {
          logout();
        }}
      >
        logout
      </button>
      <button
        // try to call the API
        onClick={async () => {
          const client = generateClient();
          client
            .graphql({
              query: listAnotherTodos,
              // used OIDC as authMode
              authMode: "oidc",
            })
            .then((x) => {
              console.log(x);
            })
            .catch((err) => {
              console.log(err);
            });
        }}
      >
        get
      </button>
    </>
  );
}

export default App;

auth0 api response

{
  "access_token": "my access token",
  "id_token": "my id token",
  "scope": "openid profile email",
  "expires_in": 86400,
  "token_type": "Bearer"
}

pass auth0 information into amplify, and call fetchAuthSession()

{
  "tokens": {
    "accessToken": {
      "payload": {
        "nickname": "my nick name",
        "name": "my name",
        "picture": "cdn url",
        "updated_at": "2024-04-16T02:39:42.626Z",
        "email": "my email",
        "email_verified": true,
        "iss": "https://my-domain.auth0.com/",
        "aud": "xxx",
        "iat": 1713236890,
        "exp": 1713272890,
        "sub": "oauth2|discord|xxx",
        "sid": "sid",
        "nonce": "nonce"
      }
    },
    "idToken": "exact same token from previous auth0 response"
  },
  "credentials": {
    "accessKeyId": "some access key",
    "secretAccessKey": "some secret",
    "sessionToken": "some token",
    "expiration": "2024-04-16T04:09:28.000Z"
  },
  "identityId": "my-identity-id",
  "userSub": "oauth2|discord|xxx"
}

Call the graphql API response

{
  "errors" : [ {
    "errorType" : "UnauthorizedException",
    "message" : "Unauthorized"
  } ]
}

header curl:

curl 'https://my-project-domain.appsync-api.ap-northeast-1.amazonaws.com/graphql' \
 -H 'accept: _/_' \
 -H 'accept-language: en-US,en;q=0.9' \
 -H 'authorization:  exact same token with auth0 response and fetchAuthSession()' \
 -H 'content-type: application/json; charset=UTF-8' \
 -H 'origin: http://localhost:5173' \
 -H 'referer: http://localhost:5173/' \
 -H 'sec-fetch-dest: empty' \
 -H 'sec-fetch-mode: cors' \
 -H 'sec-fetch-site: cross-site' \
 -H 'user-agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1' \
 -H 'x-amz-user-agent: aws-amplify/6.0.27 api/1 framework/1' \
 --data-raw '{"query":"query ListAnotherTodos($filter: ModelAnotherTodoFilterInput, $limit: Int, $nextToken: String) {\n listAnotherTodos(filter: $filter, limit: $limit, nextToken: $nextToken) {\n items {\n id\n name\n description\n sub\n createdAt\n updatedAt\n owner\n **typename\n }\n nextToken\n **typename\n }\n}\n","variables":{}}'

schema.graphql

type AnotherTodo
  @model
  @auth(
    rules: [
      { allow: owner, provider: oidc, identityClaim: "sub" }
    ]
  ) {
  id: ID!
  name: String!
  description: String!
  sub: String!
}

Log output

// Put your logs below this line


aws-exports.js

/* eslint-disable */
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
"aws_project_region": "ap-northeast-1",
"aws_appsync_graphqlEndpoint": "https://my-domain.appsync-api.ap-northeast-1.amazonaws.com/graphql",
"aws_appsync_region": "ap-northeast-1",
"aws_appsync_authenticationType": "API_KEY",
"aws_appsync_apiKey": "my key",
"aws_cognito_identity_pool_id": "my id",
"aws_cognito_region": "ap-northeast-1",
"aws_user_pools_id": "ap-northeast-my-id",
"aws_user_pools_web_client_id": "my id",
"oauth": {
"domain": "my-domain-staging.auth.ap-northeast-1.amazoncognito.com",
"scope": [
"phone",
"email",
"openid",
"profile",
"aws.cognito.signin.user.admin"
],
"redirectSignIn": "http://localhost:5173/",
"redirectSignOut": "http://localhost:5173/",
"responseType": "code"
},
"federationTarget": "COGNITO_USER_POOLS",
"aws_cognito_username_attributes": [
"EMAIL"
],
"aws_cognito_social_providers": [],
"aws_cognito_signup_attributes": [
"EMAIL",
"NAME"
],
"aws_cognito_mfa_configuration": "OFF",
"aws_cognito_mfa_types": [
"SMS"
],
"aws_cognito_password_protection_settings": {
"passwordPolicyMinLength": 8,
"passwordPolicyCharacters": []
},
"aws_cognito_verification_mechanisms": [
"EMAIL"
]
};

export default awsmobile;

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

@TitusEfferian TitusEfferian added the pending-triage Issue is pending triage label Apr 16, 2024
@cwomack cwomack added the GraphQL Related to GraphQL API issues label Apr 16, 2024
@TitusEfferian
Copy link
Author

hi @cwomack any update for this? 🙇

@chrisbonifacio
Copy link
Contributor

Hi @TitusEfferian it looks like your schema has a sub field but that field is not used as the ownerField in the auth rule. This suggests that Amplify is adding an owner field to the record behind the scenes, which you should be able to see in the DynamoDB console when viewing table items for the model. Can you please confirm that the value being set for owner matches the sub claim on the id token?

I think it's also worth mentioning that your myTokenProvider function is returning both the id and access token. By default, Amplify will send the access token in the Authorization header. Can you please check the outgoing graphql requests in your Network activity and make sure that the token_use claim is what you expect? (in this case id).

@chrisbonifacio chrisbonifacio added pending-response Issue is pending response from the issue requestor question General question and removed pending-triage Issue is pending triage labels Apr 29, 2024
@TitusEfferian
Copy link
Author

Hi @TitusEfferian it looks like your schema has a sub field but that field is not used as the ownerField in the auth rule. This suggests that Amplify is adding an owner field to the record behind the scenes, which you should be able to see in the DynamoDB console when viewing table items for the model. Can you please confirm that the value being set for owner matches the sub claim on the id token?

I am experimenting with a completely new project, so currently I don't have any items in DynamoDB. I tried creating new data but encountered the same error. Here, I have already attempted to modify the schema again.

type AnotherTodo
  @model
  @auth(rules: [{ allow: owner, provider: oidc, identityClaim: "sub" }]) {
  id: ID!
  name: String!
  description: String!
}
const client = generateClient();
const input: CreateAnotherTodoInput = {
  description: "hello",
  name: "hello",
};
client
  .graphql({
    query: createAnotherTodo,
    authMode: "oidc",
    variables: {
      input,
    },
  })
  .then((x) => {
    console.log(x);
  })
  .catch((err) => {
    console.log(err);
  });
curl 'https://gps6v37nqjhchcxt22t7awvhaq.appsync-api.ap-northeast-1.amazonaws.com/graphql' \
 -H 'accept: _/_' \
 -H 'accept-language: en-US,en;q=0.9' \
 -H 'authorization: token' \
 -H 'content-type: application/json; charset=UTF-8' \
 -H 'origin: http://localhost:5173' \
 -H 'priority: u=1, i' \
 -H 'referer: http://localhost:5173/' \
 -H 'sec-fetch-dest: empty' \
 -H 'sec-fetch-mode: cors' \
 -H 'sec-fetch-site: cross-site' \
 -H 'user-agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1' \
 -H 'x-amz-user-agent: aws-amplify/6.0.27 api/1 framework/1' \
 --data-raw $'{"query":"mutation CreateAnotherTodo($input: CreateAnotherTodoInput\u0021, $condition: ModelAnotherTodoConditionInput) {\\n createAnotherTodo(input: $input, condition: $condition) {\\n id\\n name\\n description\\n createdAt\\n updatedAt\\n owner\\n \_\_typename\\n }\\n}\\n","variables":{"input":{"description":"hello","name":"hello"}}}'

I think it's also worth mentioning that your myTokenProvider function is returning both the id and access token. By default, Amplify will send the access token in the Authorization header. Can you please check the outgoing graphql requests in your Network activity and make sure that the token_use claim is what you expect? (in this case id).

Yes, I can confirm that the token sent in the GraphQL request is the same one that I placed in myTokenProvider. I can also confirm that the structure of the JWT contains the sub fields, as I mentioned.

@github-actions github-actions bot removed the pending-response Issue is pending response from the issue requestor label Apr 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
GraphQL Related to GraphQL API issues question General question
Projects
None yet
Development

No branches or pull requests

3 participants