microsoft-authentication-library-for-js: Cannot log in using a mobile and desktop applications redirect URL prefixed with `tauri://`

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

2.38.0

Wrapper Library

MSAL React (@azure/msal-react)

Wrapper Library Version

1.5.9

Public or Confidential Client?

Public

Description

Cannot log in using a mobile and desktop applications redirect URL prefixed with tauri://. It works with http://localhost but not from tauri://localhost.

Error Message

{
    "error": "invalid_request",
    "error_description": "AADSTS90023: Cross-origin token redemption is permitted only for the 'Single-Page Application' client-type or 'Native' client-type with origin registered in AllowedOriginForNativeAppCorsRequestInOAuthToken allow list.\r\nTrace ID: xxx\r\nCorrelation ID: xxx\r\nTimestamp: 2023-07-17 16:28:13Z",
    "error_codes": [
        90023
    ],
    "timestamp": "2023-07-17 16:28:13Z",
    "trace_id": "xxx",
    "correlation_id": "xxx"
}

MSAL Logs

[Debug] Permission granted (index-95a4e7e3.js, line 107)
[Error] Unhandled Promise Rejection: Scope not defined for window `main` and URL `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=xxx&scope=openid%20profile%20offline_access&redir...
[Info] Successfully preconnected to https://aadcdn.msauth.net/ (x2)
[Warning] [TAURI] Couldn't find callback id 1866876788 in window. This happens when the app is reloaded while Rust is running an asynchronous operation. (authorize, line 5)
[Error] Unhandled Promise Rejection: Scope not defined for window `main` and URL `https://login.live.com/oauth20_authorize.srf?client_id=xxx&scope=openid+profile+offline_access&redirect_uri=tauri%3a%2f%2f...
[Error] Unhandled Promise Rejection: Scope not defined for window `main` and URL `https://account.live.com/App/Confirm?mkt=EN-GB&uiflavor=host&id=293577&client_id=0000000048FB303C&ru=https://login.live.com/oauth20_authorize.srf%3fuaid%3d...
[Info] Successfully preconnected to https://acctcdn.msftauth.net/ (x4)
[Log] Mon, 17 Jul 2023 16:37:45 GMT:FlowController.showControl(appConfirm) (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:45 GMT:New State [appConfirm] from [none] (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:45 GMT:Hooking control events for [appConfirm] (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:45 GMT:PageDialogControl.show() (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:45 GMT:FlowController.handleControlEvent [onSetupEvents] for [appConfirm] (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:45 GMT:FlowController.handleControlEvent [onShow] for [appConfirm] (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:45 GMT:PageDialogControl.~show() (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:45 GMT:FlowController.notifyVisible [appConfirm] (Confirm, line 78)
[Error] Failed to load resource: the server responded with a status of 404 () (CustomFunctions.js.map, line 0)
[Error] Failed to load resource: the server responded with a status of 404 () (ms.properties-3.2.6.min.js.map, line 0)
[Log] Mon, 17 Jul 2023 16:37:47 GMT:FlowController.handleControlEvent [onAction] for [appConfirm] (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:47 GMT:FlowController.processActionEvent[next] for [appConfirm] (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:47 GMT:FlowController.processActionEvent newState [success] (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:47 GMT:FlowController.showControl(success) (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:47 GMT:New State [success] from [appConfirm] (Confirm, line 78)
[Log] Mon, 17 Jul 2023 16:37:47 GMT:Navigate to [https://login.live.com/oauth20_authorize.srf?uaid=65df5d6b0986473e8dcae64a41db9303&client_id=xxx&opid=39F05E265291377E&mkt=EN-GB&opidt=1689611864&route=C107_BL2&res=success] (Confirm, line 78)
[Debug] Permission granted (index-95a4e7e3.js, line 107)
[Error] Failed to load resource: the server responded with a status of 400 (Bad Request) (token, line 0)

MSAL Configuration

const pcaConfig = {
  auth: {
    clientId: import.meta.env.VITE_OIDC_CLIENT_ID,
    navigateToLoginRequestUrl: true, // Go back to the original page after login
    postLogoutRedirectUri: "/", // Go back to the app root after logout
    redirectUri: "/", // Go back to the app root after login
  },
  cache: {
    cacheLocation: "localStorage",
    temporaryCacheLocation: "sessionStorage",
  },
  system: {
    navigationClient: new CustomNavigationClient(router.navigate),
    loggerOptions: {
      logLevel: import.meta.env.DEV || import.meta.env.TAURI_DEBUG ? LogLevel.Verbose : LogLevel.Warning,
      loggerCallback: (level, message, containsPii) => {
        if (containsPii) {
          return;
        }
        switch (level) {
          case LogLevel.Error:
            console.error(message);
            return;
          case LogLevel.Info:
            console.info(message);
            return;
          case LogLevel.Verbose:
            console.debug(message);
            return;
          case LogLevel.Warning:
            console.warn(message);
            return;
        }
      },
      piiLoggingEnabled: false,
    },
  },
};

Relevant Code Snippets

The application is run from Tauri (embedded web app on desktop, no Node.js runtime).

const IS_TAURI = window.__TAURI_METADATA__ != undefined;

const login = async (instance) => {
  IS_TAURI ? await instance.loginRedirect() : await instance.loginPopup();
};

const getIdToken = async (account, instance) => {
  if (!account) return null;

  const req = {
    account: account,
    loginHint: account.username,
    scopes: ["openid", "profile", "email", "User.Read"],
  };

  // Try silent first
  const idToken = await instance
    .acquireTokenSilent(req)
    .then((res) => {
      return res.idToken;
    })
    .catch((error) => {
      if (!(error instanceof InteractionRequiredAuthError)) {
        console.error(error);
        return null;
      }

      const onSuccess = (res) => {
        return res.idToken;
      };

      const onError = (error) => {
        console.error(error);
        return null;
      };

      if (IS_TAURI) {
        // Failback to redirect
        return instance
          .acquireTokenRedirect(req)
          .then(onSuccess)
          .catch(onError);
      }

      // Failback to popup
      return instance.acquireTokenPopup(req).then(onSuccess).catch(onError);
    });

  return idToken;
};

AAD app manifest:

{
	"id": "xxx",
	"acceptMappedClaims": null,
	"accessTokenAcceptedVersion": 2,
	"addIns": [],
	"allowPublicClient": true,
	"appId": "e9d5f20f-7f14-4204-a9a2-0d91d6af5c82",
	"appRoles": [
		{
			"allowedMemberTypes": [
				"User"
			],
			"description": "Ability to access the application.",
			"displayName": "Contributors",
			"id": "687bbba0-26c1-435e-9e48-5cdd93d423cb",
			"isEnabled": true,
			"lang": null,
			"origin": "Application",
			"value": "Contributors"
		}
	],
	"oauth2AllowUrlPathMatching": false,
	"createdDateTime": "2023-06-28T12:43:54Z",
	"description": null,
	"certification": null,
	"disabledByMicrosoftStatus": null,
	"groupMembershipClaims": "None",
	"identifierUris": [],
	"informationalUrls": {
		"termsOfService": null,
		"support": null,
		"privacy": null,
		"marketing": null
	},
	"keyCredentials": [],
	"knownClientApplications": [],
	"logoUrl": "https://aadcdn.msftauthimages.net/dbd5a2dd-arfqpbjsje9u4fcbla9kbny3eqocpihhyg0ntigcgqg/appbranding/kjco3fpdmdpzymxko05usfexpyybabekbazxi4jp40y/1033/bannerlogo?ts=638251791039670967",
	"logoutUrl": null,
	"name": "Private GPT",
	"notes": null,
	"oauth2AllowIdTokenImplicitFlow": true,
	"oauth2AllowImplicitFlow": false,
	"oauth2Permissions": [],
	"oauth2RequirePostResponse": false,
	"optionalClaims": {
		"idToken": [
			{
				"name": "login_hint",
				"source": null,
				"essential": false,
				"additionalProperties": []
			}
		],
		"accessToken": [],
		"saml2Token": []
	},
	"orgRestrictions": [],
	"parentalControlSettings": {
		"countriesBlockedForMinors": [],
		"legalAgeGroupRule": "Allow"
	},
	"passwordCredentials": [],
	"preAuthorizedApplications": [],
	"publisherDomain": "xxx.onmicrosoft.com",
	"replyUrlsWithType": [
		{
			"url": "https://private-gpt.shopping-cart-devops-demo.lesne.pro",
			"type": "Spa"
		},
		{
			"url": "http://localhost:8080",
			"type": "Spa"
		},
		{
			"url": "tauri://localhost",
			"type": "InstalledClient"
		},
		{
			"url": "https://private-gpt.shopping-cart-devops-demo.lesne.pro/auth",
			"type": "Spa"
		}
	],
	"requiredResourceAccess": [
		{
			"resourceAppId": "00000003-0000-0000-c000-000000000000",
			"resourceAccess": [
				{
					"id": "14dad69e-099b-42c9-810b-d002981feec1",
					"type": "Scope"
				},
				{
					"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
					"type": "Scope"
				},
				{
					"id": "37f7f235-527c-4136-accd-4a02d197296e",
					"type": "Scope"
				},
				{
					"id": "64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0",
					"type": "Scope"
				}
			]
		}
	],
	"samlMetadataUrl": null,
	"signInUrl": "https://github.com/clemlesne/private-gpt",
	"signInAudience": "AzureADandPersonalMicrosoftAccount",
	"tags": [
		"NoLiveSdkSupport"
	],
	"tokenEncryptionKeyId": null
}

Expected Behavior

I would expect the logging to work the same it is working for the web version.

Identity Provider

Azure AD / MSA

Browsers Affected (Select all that apply)

Other

Related links (suggested)

Source

Internal (Microsoft)

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 16 (13 by maintainers)

Most upvoted comments

Yes, our verification of redirectUri depends on the https messaging to the service. Since it is a SPA and no secrets are exchanged, this is the only way we can confirm the app’s validity to receive the tokens.

More docs can be found here.

@clemlesne SPAs only support only URLs that start with https: for production apps and http://localhost for local dev. The format you mentioned above can be supported only for mobile or web apps as they have a confidential component unlike browser apps. Hope this clarifies.

This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @konstantin-msft please follow up.