Hi, MS Identity team,
First, I want to say that I appreciate the work you guys are doing to simplify auth as a whole. It's my weakest point in development, by far. I just feel like there's something I'm not getting and your work is helping. So here's my situation...
I have a .Net Core 3.1 Web API I want to protect with an AAD-issued bearer access token. I'm getting the token using MSAL and that is working fine. I've verified the token at jwt.ms and everything looks good: aud
is my API's client ID from AAD, scp
is my custom scope that I'm requesting, and iss
is login.microsoftonline.com/<my tenant id>
as expected. I get the token with MSAL and add it to my request using JavaScript fetch
and adding the token to the 'Authentication' header with the value Bearer <token>
.
If I leave off the [Authorize]
attribute in the web API (allowing anonymous access), I can see that the token is indeed included in the request headers and that the value is the same as it should be. However, when I add the [Authorize]
attribute, the response is a 401 and no code is being run in the API.
Details of my configurations are below. Please let me know what other information I can provide to figure this out. If need be, also let me know if there's a better place to ask for this type of help.
CLIENT
msalConfig.js
import * as msal from '@azure/msal-browser'
// Config object to be passed to Msal on creation
export const msalConfig = {
auth: {
clientId: "...my client id...",
authority: "https://login.microsoftonline.com/... my tenant id ...",
navigateToLoginRequestUrl: false
},
cache: {
cacheLocation: "sessionStorage", // This configures where your cache will be stored
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) {
return;
}
switch (level) {
case msal.LogLevel.Error:
console.error(message);
return;
case msal.LogLevel.Info:
console.info(message);
return;
case msal.LogLevel.Verbose:
console.debug(message);
return;
case msal.LogLevel.Warning:
console.warn(message);
return;
}
}
}
}
};
// Add here scopes for id token to be used at MS Identity Platform endpoints.
export const loginRequest = {
scopes: ["User.Read"]
};
// Add here the endpoints for MS Graph API services you would like to use.
export const graphConfig = {
graphMeEndpoint: "https://graph.microsoft-ppe.com/v1.0/me",
graphMailEndpoint: "https://graph.microsoft-ppe.com/v1.0/me/messages"
};
// Add here scopes for access token to be used at MS Graph API endpoints.
export const tokenRequest = {
scopes: ["Mail.Read"],
forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token
};
export const silentRequest = {
scopes: ["openid", "profile", "User.Read", "Mail.Read"]
};
export const apiRequest = {
scopes: ["api://...my client id.../Access.Grant"]
}
auth.js
import * as msal from '@azure/msal-browser'
import {
msalConfig,
loginRequest
} from './msalConfig'
// Browser check variables
// If you support IE, our recommendation is that you sign-in using Redirect APIs
// If you as a developer are testing using Edge InPrivate mode, please add "isEdge" to the if check
const ua = window.navigator.userAgent;
const msie = ua.indexOf("MSIE ");
const msie11 = ua.indexOf("Trident/");
// const msedge = ua.indexOf("Edge/");
const isIE = msie > 0 || msie11 > 0;
// const isEdge = msedge > 0;
let signInType;
let username = "";
// Create the main myMSALObj instance
// configuration parameters are located at authConfig.js
const myMSALObj = new msal.PublicClientApplication(msalConfig);
// Redirect: once login is successful and redirects with tokens, call Graph API
myMSALObj.handleRedirectPromise().then(handleResponse).catch(err => {
console.error(err);
});
function handleResponse(resp) {
if (resp !== null) {
username = resp.account.username;
// showWelcomeMessage(resp.account);
} else {
// need to call getAccount here?
const currentAccounts = myMSALObj.getAllAccounts();
if (!currentAccounts || currentAccounts.length < 1) {
return;
} else if (currentAccounts.length > 1) {
// Add choose account code here
} else if (currentAccounts.length === 1) {
username = currentAccounts[0].username;
// showWelcomeMessage(currentAccounts[0]);
}
}
}
export async function getAccounts() {
let accounts = myMSALObj.getAllAccounts();
return accounts;
}
export async function signIn(method) {
signInType = isIE ? "loginRedirect" : method;
if (signInType === "loginPopup") {
try {
let response = await myMSALObj.loginPopup(loginRequest);
return response;
}
catch (err) {
console.error(err);
}
} else if (signInType === "loginRedirect") {
return myMSALObj.loginRedirect(loginRequest)
}
}
export function signOut() {
const logoutRequest = {
account: myMSALObj.getAccountByUsername(username)
};
myMSALObj.logout(logoutRequest);
}
export async function getTokenPopup(request, account) {
request.account = account;
return await myMSALObj.acquireTokenSilent(request).catch(async (error) => {
console.log("silent token acquisition fails.");
if (error instanceof msal.InteractionRequiredAuthError) {
console.log("acquiring token using popup");
return myMSALObj.acquireTokenPopup(request).catch(error => {
console.error(error);
});
} else {
console.error(error);
}
});
}
// This function can be removed if you do not need to support IE
export async function getTokenRedirect(request, account) {
request.account = account;
return await myMSALObj.acquireTokenSilent(request).catch(async (error) => {
console.log("silent token acquisition fails.");
if (error instanceof msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
console.log("acquiring token using redirect");
myMSALObj.acquireTokenRedirect(request);
} else {
console.error(error);
}
});
}
Vue component (only showing script tag for brevity)
<script>
import * as auth from '../msal';
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
user: {},
requests: { ...auth.requests },
holds: []
}
},
methods: {
async login() {
let response = await auth.signIn('loginPopup');
this.user = {...response.account};
},
async getApiToken() {
let response = await auth.getTokenPopup(this.requests.api, this.user);
console.log(response);
return response;
},
async getData() {
let tokenResponse = await this.getApiToken();
let response = await fetch('https://localhost:4001/api/data', { headers: new Headers ({ "Authentication": `Bearer ${tokenResponse.accessToken}`})});
if (response.status == 401) {
// eslint-disable-next-line no-debugger
debugger
console.log('nope')
} else {
let data = await response.json();
this.holds = data;
}
}
},
computed: {
auth() {
return auth;
}
}
}
</script>
API
Startup.cs (only showing relevant parts)
public void ConfigureServices (IServiceCollection services)
{
services.AddMicrosoftWebApiAuthentication(Configuration);
services.AddCors();
services.AddControllers();
services.AddHttpContextAccessor();
services.AddDbContext<ConfigContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Config")));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure (IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseCors(o => o.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
appSettings.json
...
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "... my domain ... .onmicrosoft.com",
"TenantId": "... my AAD tenant id ...",
"ClientId": "... my API's AAD client id ..."
}
...