0

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! `

Matt P
  • 25
  • 7

0 Answers0