- Overview
- Call ARM from the client
- Calling backend services/APIs or Graph APIs
- Access claims
- Migrating resource access configuration
The Portal uses an internal provider for authentication and authorization. The built-in cloud authentication provider uses Azure Active Directory (AAD), which also supports Microsoft Account users. If your extension needs to differentiate between AAD and MSA users, see Access claims from the extension server. If you are working on first-party extensions, but you are not sure that your extension scenarios comply with Azure terms and conditions, you may want to reach out to ibiza-lca@microsoft .com.
During sign-in, the Portal obtains a token that contains claims that identify the signed-in user. The Portal also retrieves directories and subscriptions to which the user has access.
NOTE: Users can sign in without a subscription. Extensions must gracefully handle this case and return zero assets when queried.
The extension can call ARM from the client. The extension can also call external services from the client or from the extension server.
Calling external services involves AAD Onboarding. The onboarding process can take five or six weeks, so if your extension needs to invoke services other than ARM, you should reach out to the Azure Portal team early in the design phase, as specified in top-extensions-onboarding-with-related-teams.md.
NOTE: Only first-party extensions can call alternate resources. Third party extensions use an encrypted token that cannot be decrypted by services other than ARM, therefore they cannot call alternate resources.
Your extension should use cross-origin resource sharing (CORS) for all non-aggregated, non-orchestrated calls.
Use the built-in ajax()
function in the Fx/Ajax
module to communicate from the extension client to call any extension server API. This function will attach a token targeted at ARM that is specific to your extension for the HTTP header.
NOTE: Do not use jQuery.ajax()
because it will not properly authorize your requests. If you have a scenario that is not supported by ajax()
, you can use the getAuthorizationToken()
function to obtain a token and manually attach it to your request.
Only Ibiza has the authority to mint tokens. To call external resources, extension developers need to request the creation of the AAD app and register the extension resources with Ibiza for the appropriate environment, as specified in top-onboarding.md#register-the-extension-with-the-portal-product-configuration.
The following example enables a sample extension Contoso_Extension
to query Graph APIs from the client.
-
Create a first party AAD application https://firstpartyportal.msidentity.com/. Ensure that the Subject Name + Issuer (SNI) used by the Portal is listed on the AAD application. The list of portal SNIs is documented at https://aka.ms/portalfx/sni.
-
Onboard your AAD application ID in the portal configuration using EasyStart per the steps outlined in https://aka.ms/portalfx/newextension.
-
Define the auth configuration in the extension's hosting service configuration file as outlined at defining extension resource access configuration
- Note: Sideloading the extension-defined resource access is not supported since the portal server will not have access to your machine to read the sideloaded configuration.
- For testing purposes, the extension configuration can be deployed to a friendly-name stamp using the steps outlined at: testing extension-side authorization configuration with friendly-name stamps
-
After the configuration changes are deployed in the requested environment, like Dogfood, PPE, or PROD, the extension can request tokens for the graph resource using any of its data APIs, as in the following code.
- To automatically attach a the token when making
ajax
calls, use thename
property defined for the resource in the extension configuration file as theresourceName
in thesetAuthorizationHeader
argument. - To get an access token for a specific resource, use the
getAuthorizationToken
call as outlined below.
import { ajax, getEndpoints } from "Fx/Ajax"; MsPortalFx.Base.Security.getAuthorizationToken({ resourceName: "graph" }); ajax({ setAuthorizationHeader: { resourceName: "graph" }, uri: getEndpoints().graph + "/endpoint", }); new MsPortalFx.ViewModels.Controls.FileDownload.ViewModel( container, { context: ko.observable(new FileDownload.Context({ ... addDefaultAuthorizationToken: { resourceName: "graph", }, ... })), }); new MsPortalFx.ViewModels.FileDownloadCommand({ authorizationOptions: { resourceName: "graph", } });
- To automatically attach a the token when making
Extensions can also exchange tokens themselves for a target resource. This can be achieved as follows:
- Include a "self" resource in the extension's resource access configuration:
"authorization": {
"resourceAccess": [
{
"name": "self",
"resource": "1a123abc-1234-1a2b-ab01-01ab01a1a1ab" // The extension's AAD application ID
}
]
}
- On the client, request a token for the "self" resource using the
ajax
function or thegetAuthorizationCode
function as demonstrated in the previous section. The following code adds a parameter to theajax
call:
MsPortalFx.Base.Net2.ajax({
uri: "MyController/MyAction",
setAuthorizationHeader: { resourceName: "self" }
}).then((myData) => {
// do work with data
});
- On the extension's controller, the token can be exchanged for another target resource as follows. Note that the following sample uses MSAL.Net.
// Get the token passed to the controller
var portalAuthorizationHeader = PortalRequestContext.Current.GetCorrelationData<AuthorizationCorrelationProvider>();
if (portalAuthorizationHeader == null)
{
// This should never happen, the auth module should have returned 401 if there wasn't a valid header present
throw new HttpException(401, "Unauthorized");
}
// Exchange it for the token that should pass to downstream services
var exchangedAuthorizationHeader = GetExchangedToken(portalAuthorizationHeader, intuneClientId, intuneClientCert, "https://graph.windows.net/");
// Call downstream service with exchanged header
var headers = new NameValueCollection();
headers.Add("Authorization", exchangedAuthorizationHeader);
webApiClient.GetAsync(uri, "MyOperation", headers);
// Helper method to exchange tokens
async Task<string> GetExchangedToken(string portalAuthorizationHeader, string clientId, X509Certificate2 clientCertificate, string resource)
{
var scopes = new string[] { resource + "./default" }; // This is the MSAL.Net requirement to request tokens for the default scope for a given resource.
var confidentialClientApp = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithCertificate(clientCertificate)
.WithAuthority(this.aadAuthority.ToString()) // This value can be obtained from the config value using Microsoft.Portal.Security.AzureActiveDirectory.IAadAuthenticationConfiguration.Authority.
.Build();
// Proof that the request originated from the portal and is on behalf of a valid user
var accessToken = GetAccessTokenFromAuthorizationHeader(portalAuthorizationHeader);
var userAssertion = new UserAssertion(accessToken, "urn:ietf:params:oauth:grant-type:jwt-bearer");
// The actual token exchange
var exchangedToken = await extensionApp.AcquireTokenOnBehalfOf(scopes, userAssertion)
.WithSendX5C(true) // This is required to use SN+I for certificate validation.
.ExecuteAsync()
.ConfigureAwait(false);
return exchangedToken.CreateAuthorizationHeader();
}
string GetAccessTokenFromAuthorizationHeader(string authorizationHeader)
{
// The header will be in the form "Bearer ey��MZ"
// The access token in the last part of the header
var separator = new char[] { ' ' };
var accessToken = authorizationHeader.Split(separator, StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
return accessToken;
}
Tokens received from AAD contain a set of claims that are formatted as key-value pairs. They contain information about the user, and they are only available to extensions that comply with the Azure privacy policy that is located at https://www.microsoft.com/en-us/TrustCenter/Privacy/default.aspx.
They may include items like a list of directories that the user can access from ARM, or a list of subscriptions that the user can access from ARM.
Extensions that are not covered by this policy, like the ones that share PII with third-parties, do not have access to the token or its claims, because Microsoft can be sued for abuse or misuse of PII as specified in the privacy policy. These exceptions need to be approved by reaching out to ibiza-lca@microsoft .com.
Extensions that have access to claims can use the getUserInfo()
API to retrieve common claims from the client.
NOTE: Secondary claims, like name and email, may not always be available and cannot be guaranteed. Token claims may change over time as AAD evolves. Call the Graph
to get required user information instead of extracting claims yourself. Do not make hard dependencies on claims.
The following code retrieves common claims from the client.
MsPortalFx.Base.Security.getUserInfo() : Promise<MsPortalFx.Base.Security.UserInfo>
interface UserInfo {
email: string; // Guest accounts not homed in the current directory will have a UPN and not a valid email address
givenName: string; // Name may be empty if not provided in the token
surname: string; // Name may be empty if not provided in the token
directoryId: string;
directoryName: string; // Directory name may be empty if calling Graph fails
domainName: string; // Directory domain may be empty if calling Graph fails
}
While not recommended, the token used to communicate with your server also contains claims that can be read from the
server by using the ASP.NET claims API specified in https://msdn.microsoft.com/en-us/library/ee517271.aspx. To simplify development, the HttpContext.User
property specified in https://aka.ms/portalfx/httpContextUser has been augmented with the most commonly used claims.
The extension uses the API to read additional claims from the token. Due to size constraints, additional information required by an extension cannot be added to the token. Instead, the extension obtains it from the AAD Graph API that is specified in https://aka.ms/portalfx/AADGraphAPI.
The following code sample retrieves common claims.
-
Reference the following assemblies:
-
Microsoft.Portal.AadCore.dll
-
System.IdentityModel.Tokens.Jwt.dll
-
-
Add the following to the web.config file.
<!-- deny anonymous users --> <system.web> <authorization> <deny users="?" /> </authorization> </system.web> <!-- allow the home page which serves the extension source --> <!-- this loads the extension from ~/Index; change path to fit your development environment --> <location path="Index"> <system.web> <authorization> <allow users="*" /> </authorization> </system.web> </location> <!-- allow images and scripts --> <location path="Content"> <system.web> <authorization> <allow users="*" /> </authorization> </system.web> </location> <location path="Scripts"> <system.web> <authorization> <allow users="*" /> </authorization> </system.web> </location> <configuration> <appSettings> <!-- For test, use authority="https://login.windows-ppe.net/" --> <!-- For PROD, use authority="https://login.windows.net/" --> <add key="Microsoft.Portal.Security.AzureActiveDirectory.AadAuthenticationConfiguration.Authority" value="https://login.windows-ppe.net/" /> <add key="Microsoft.Portal.Security.AzureActiveDirectory.AadAuthenticationConfiguration.TenantId" value="common" /> <add key="Microsoft.Portal.Security.AzureActiveDirectory.AadAuthenticationConfiguration.AllowedAudiences" value="['https://management.core.windows.net/']" /> <add key="Microsoft.Portal.Security.AzureActiveDirectory.AadAuthenticationConfiguration.MinValidationCertUpdateInterval" value="PT05M" /> <add key="Microsoft.Portal.Security.AzureActiveDirectory.AadAuthenticationConfiguration.MaxValidationCertUpdateInterval" value="PT24H" /> <add key="Microsoft.Portal.Security.AzureActiveDirectory.AadAuthenticationConfiguration.ForwardDecryptedAuthorizationTokens" value="false" /> </appSettings> </configuration>
-
Use
HttpContext.User
to retrieve the common claims, as in the following code.// use IPortalIdentity for email and tenant id // NOTE: Do not rely on IPortalIdentity.FirstName and LastName properties; they are not consistenty populated var portalUser = HttpContext.User.Identity as Microsoft.Portal.Framework.IPortalIdentity; // portalUser.EmailAddress; // portalUser.TenantId; // use IAadIdentity (implements IPortalIdentity) for user id // and to determine if the user is an MSA or AAD account var aadUser = portalUser as Microsoft.WindowsAzure.Management.AadAuthentication.IAadIdentity; if (aadUser != null) { // aadUser.ObjectId; // aadUser.PrincipalId; // aadUser.IsOrgId }
For more information about default claims that are provided by AAD, see the "Azure AD token reference" article located at https://aka.ms/portalfx/tokensandclaims.
The Portal does not automatically log users out after a period of inactivity; it only logs a user out when the user's AAD-issued authentication token expires. Otherwise, all subsequent operations would fail.
This typically happens after a few hours of usage, maybe eight to 24 hours based on the type of account. It may also happen if the token was not refreshed or renewed for a period of time, which is typically one hour.
NOTE: If the browser is in a situation where it cannot connect to the network for more than an hour, typically, the user is logged out.
To provide extension authors greater control over the deployment cadence of resource access changes, the SDK now supports defining resource access in the extension's hosting service configuration.
-
Prerequisite: If your extension requires access to a resource other than ARM, you will need to provide an
oAuthClientId
in the EasyStart Onboarding tool for your extension. If you only need access to ARM, you do not need to define aresourceAccess
section in your extension's configuration. -
Go to your extension's 1P app by visiting https://aka.ms/firstpartyportal. Pre-authorize the portal's client ID in that respective cloud with the "user_impersonation" scope. This is required to allow the portal to be able to fetch tokens on behalf of the extension's 1P app. (For quick reference, portal's clientID in fairfax is
c836cbdb-7a5b-44cc-a54f-564b4b486fc6
. Portal's client ID in all other clouds isc44b4083-3bb0-49c1-b47d-974e53cbdf3c
) -
Add the desired resources to your extension's hosting service configuration.
- In your extension's hosting service configuration(s), add a new
authorization
property with the desired resource access.- Note: the ARM resource (name: "") is not included. All extensions will now get the portal's ARM access token by default without needing to define it in configuration.
{ "authorization": { "resourceAccess": [ { "name": "microsoft.graph", "resource": "https://graph.microsoft.com/" } ] } }
- In your extension's hosting service configuration(s), add a new
-
Deploy your extension with the new configuration changes.
- Your configuration changes must be fully deployed to all stages in order to be detected by the authentication pipeline.
- Optional: Resource access configuration changes can be tested by deploying to a friendly-name stamp. Instructions can be found below.
-
Verify your new configuration by requesting a token for each of the resources with access defined using the following methods:
-
From a blade in your extension.
-
Manually from the developer command prompt:
-
From your browser's developer tools, navigate to the "Console" tab, and select your extension's frame using the dropdown in the header.
-
The following code will trigger a network request to
/api/DelegationToken
for your resource:
MsPortalFx.Base.Security.getAuthorizationToken({ resourceName: "microsoft.graph", });
-
Open the "Network" tab of your browser's developer tools
-
Locate the call to
/api/DelegationToken
and inspect the response - a successful token request will have status200
and will include a "Bearer" token in the response body.
-
-
-
Please note: Especially for extensions migrating their resource access from the Extensions.*.json files, please be aware that one of the changes this entails is that the extension client ID ('oAuthClientId' property) defined at the extension level in the portal configuration (Extensions.*.json) file will be used for all token fetches. The new resource access does not allow you to override this 'oAuthClientId' value in any way. It is important to test your new flows and inspect the access tokens used to make the all your extension's resource calls. To see the client ID used to on the requested token, you can grab the token from the authorization header of the request to the target resource, and parse that access token in https://jwt.ms. Then look at the "appId" claim and ensure that it matches the one defined at the extension level in the portal configuration. If you have any questions, feel free to reach out on the Teams channel linked in this doc.
Before migration
Extensions.prod.json
{
"name": "Contoso_Extension",
"feedbackEmail": "myteamtriage@microsoft.com",
"flags": "SupportsPrewarming",
"resourceAccess": [
{
"name": "",
"resource": "https://management.azure.com/",
}
{
"name": "microsoft.graph",
"resource": "https://graph.microsoft.com/",
}
],
"hostingServiceName": "contoso"
},
After completing migration
portal.azure.com.json
{
"authorization": {
"resourceAccess": [
{
"name": "microsoft.graph",
"resource": "https://graph.microsoft.com/"
}
]
},
}
Are you making changes to your extension's resource access that might negatively impact users? The Portal authentication pipeline now supports testing resourceAccess
changes against a friendly name stamp.
-
Deploy your extension to a friendly name stamp. More information about friendly names can be found here.
-
Load the Portal in your favorite web browser with this query string, replacing
Contoso_Extension
with your extension's name, andfoobar
with the name of your stamp:?Contoso_Extension_authstamp=foobar
All token requests for this Portal session will attempt to read your extension's authorization configuration from the provided friendly name stamp.
- Note: Portal authentication performance for your extension will be significantly degraded within this session. This feature should be used for testing and verification purposes only!
For additional questions, please join our teams channel at http://aka.ms/portalfx/resourceaccessteams