I am developing a SPA
entirely in node.js/react
that talks to an external identity server
instance (we don't manage it) using the oidc-client
module.
As for the client SPA
itself, I was able to manage the login and logout phases.
This SPA
, however, calls an external web api
project (managed by us) developed in .net framework 4.8
to get the data
The people I am talking to wanted to register my SPA
as a client in their identityserver and not the web api
project.
I wanted to know if once I get the token on the client, I can manage a flow of permissions based on that token on my web api
.
This is my authService
and its configuration:
// configuration:
public static configDev: UserManagerSettings = {
authority: 'https://idservices.servicehosting.it/,
client_id: '<the client id of my spa>',
client_secret: '<the client secret of my spa>',
redirect_uri: 'https://localhost:9099/signin-callback',
silent_redirect_uri: 'https://localhost:9099/silent-renew',
response_type: 'code',
scope: 'openid profile email roles',
post_logout_redirect_uri: 'https://localhost:9099/signout-oidc',
loadUserInfo: true,
revokeAccessTokenOnSignout: true,
};
// service
import { UserManager, Log, User } from 'oidc-client';
import Constants from '../helpers/const/Constants';
export const AuthenticationResultStatus = {
Redirect: 'redirect',
Success: 'success',
Fail: 'fail',
};
export type UserState = {
redirectPath?: string;
};
export type AuthResponse = {
status: string;
message?: string;
userState?: UserState;
};
function buildError(message: string): AuthResponse {
return { status: AuthenticationResultStatus.Fail, message };
}
function buildSuccess(userState: UserState): AuthResponse {
return { status: AuthenticationResultStatus.Success, userState };
}
function buildRedirect(): AuthResponse {
return { status: AuthenticationResultStatus.Redirect };
}
type CallbackSub = {
callback: () => void;
subscription: number;
};
export class AuthService {
private userManager: UserManager;
private callbacks: CallbackSub[] = [];
private nextSubscriptionId = 0;
private currentUser: User = null;
public constructor() {
this.userManager = new UserManager(Constants.globalOidcConfig);
// Logger
Log.logger = console;
Log.level = Log.DEBUG;
this.userManager.events.addAccessTokenExpiring(() => {
console.log('token expiring');
void this.trySilentSignIn();
});
this.userManager.events.addAccessTokenExpired(() => {
console.log('token expired');
void this.userManager.removeUser().then(() => {
this.updateState(null);
});
});
this.userManager.events.addSilentRenewError((e) => {
console.log('silent renew error', e.message);
});
this.userManager.events.addUserLoaded((user) => {
console.log('user loaded', user);
});
this.userManager.events.addUserUnloaded(() => {
console.log('user unloaded');
});
this.userManager.events.addUserSignedIn(() => {
console.log('user logged in to the token server');
});
this.userManager.events.addUserSignedOut(() => {
console.log('user logged out of the token server');
});
}
updateState(user: User): void {
this.currentUser = user;
this.notifySubscribers();
}
subscribe(callback: () => Promise<void>): number {
this.callbacks.push({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
callback,
subscription: this.nextSubscriptionId,
});
this.nextSubscriptionId += 1;
return this.nextSubscriptionId - 1;
}
unsubscribe(subscriptionId: number): void {
const subscriptionIndex = this.callbacks
.map((element, index) => (element.subscription === subscriptionId ? { found: true, index } : { found: false }))
.filter((element) => element.found === true);
if (subscriptionIndex.length !== 1) {
throw new Error(`Found an invalid number of subscriptions ${subscriptionIndex.length}`);
}
this.callbacks.splice(subscriptionIndex[0].index, 1);
}
notifySubscribers(): void {
for (let i = 0; i < this.callbacks.length; i++) {
const { callback } = this.callbacks[i];
callback();
}
}
async getUser(): Promise<User> {
if (this.currentUser?.profile == null) {
this.currentUser = await this.userManager.getUser();
}
return this.currentUser;
}
async getAccessToken(): Promise<string> {
const user = await this.userManager.getUser();
return user && user.access_token;
}
async trySilentSignIn(): Promise<User> {
await this.userManager
.signinSilent()
.then((user: User) => {
this.updateState(user);
return user;
})
.catch((error: Error) => {
void this.userManager.getUser().then((user: User) => {
console.log('silent renew error', error.message);
this.updateState(user);
return undefined;
});
});
return undefined;
}
// We try to authenticate the user in two different ways:
// 1) We try to see if we can authenticate the user silently. This happens
// when the user is already logged in on the IdP and is done using a hidden iframe
// on the client.
// 3) If the method above fails, we redirect the browser to the IdP to perform a traditional
// redirect flow.
async signin(userState: UserState): Promise<AuthResponse> {
try {
const silentUser = await this.userManager.signinSilent({ useReplaceToNavigate: true, state: userState });
this.updateState(silentUser);
return buildSuccess(silentUser.state as UserState);
} catch (silentError) {
// User might not be authenticated, fallback to redirect
console.log('Silent authentication error: ', silentError);
try {
await this.userManager.signinRedirect({ useReplaceToNavigate: true, state: userState });
return buildRedirect();
} catch (redirectError) {
console.log('Redirect authentication error: ', redirectError);
return buildError(redirectError as string);
}
}
}
async completeSignin(url?: string): Promise<AuthResponse> {
try {
const user = (await this.getUser()) || (await this.userManager.signinCallback(url));
const userState = user.state as UserState;
this.updateState(user);
return buildSuccess(userState);
} catch (error) {
console.log('There was an error signing in: ', error);
return buildError('There was an error signing in.');
}
}
// Redirect the browser to the IdP to perform a traditional
// post logout redirect flow.
async signout(): Promise<AuthResponse> {
try {
await this.userManager.signoutRedirect();
return buildRedirect();
} catch (redirectSignOutError) {
console.log('Redirect signout error: ', redirectSignOutError);
return buildError(redirectSignOutError as string);
}
}
async completeSignout(url?: string): Promise<AuthResponse> {
try {
await this.userManager.signoutCallback(url);
this.updateState(null);
return buildSuccess(null);
} catch (error) {
console.log('There was an error trying to log out ', error);
return buildError(error as string);
}
}
}
const authService = new AuthService();
export default authService;
As for the web api
, for now I have only added this in my OWIN startup class
:
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new List<string> { "openid", "profile", "email", "roles" },
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = false,
ValidIssuer = "https://localhost:9099"
ValidAudience = "https://localhost:9099", // <- I think this is wrong
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(ConfigurationManager.AppSettings["JwtKey"]))
}
});
and this in WebApiConfig
to add the bearer token authentication filter instead of its default one:
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
how could i set my webapi to correctly receive the token from my client (the spa)? thank you! `