In this blog, we will learn how you can integrate Spartacus – a composable storefront with Microsoft Azure Active Directory to achieve Single Sign On.

TLDR; Demo Video is at the end of this blog post if you are in a hurry :smiling_face_with_smiling_eyes: !!

In this article, I will cover both the changes required for Composable Storefront and SAP Commerce.

What’s the need – Latest Spartacus 6.2 version does not provide any OOTB feature/library to integrate with Microsoft Azure Active Directory for Single Sign On.

When writing this article, Composable Storefront 6.2 and SAP Commerce 2211 patch 9 has been released. The same implementation can be re-used for new or older versions.

Before diving into the implementation, I would recommend reading the document on the Single-page application: App registration

Pre-requisite  – Before starting the implementation. I assume that you have already installed the latest SAP Commerce with Spartacus sample data and also created the Composable Storefront app with the latest version. More information on how to install both has been clearly described in the SAP Help. 

An important step while creating the angular store is that you have set the routing as true. Below is the command to create a store.

 cmd : ng new spa-microsoft-login --style=scss --routing=true

High-Level Steps  – The approach to accomplish SSO will be similar to the existing implementation of Spartacus authentication with Customer Data Cloud.

 

    • Get the configuration from the Azure portal for the new app and update the configuration in the Spartacus configuration module.

 

    • Override the LoginForm Component in Spartacus and add the Logic for Authentication.

 

    • Create a new extension in SAP commerce (spasso – name of the extension).

 

    • Override the token generator to call the custom token generator in SAP Commerce.

 

    • Read the JWT token from Azure and create the customer in SAP Commerce if not found.

 

    • Generate an Authentication token from SAP Commerce and send it back to Spartacus.

 

    • Once Token is sent back to Spartacus. Users can browse through the Catalog as Logged In Users.

 

    • If the User clicks on Logout or SAP Commerce Token is expired, Revoke the token from SAP Commerce and redirect the user to the logout page of MSAL.

 

 

Auth Code Flow

Spartacus Implementation 

Step 1:  Get the configuration of your registered app from the Azure portal and update it in app-module.ts.

import { HttpClientModule } from "@angular/common/http";
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { MsalModule, MsalRedirectComponent } from "@azure/msal-angular";
import { PublicClientApplication } from "@azure/msal-browser";
import { EffectsModule } from "@ngrx/effects";
import { StoreModule } from "@ngrx/store";
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SpartacusModule } from './spartacus/spartacus.module';

const isIE =
  window.navigator.userAgent.indexOf("MSIE ") > -1 ||
  window.navigator.userAgent.indexOf("Trident/") > -1;

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule,
    StoreModule.forRoot({}),
    EffectsModule.forRoot([]),
    SpartacusModule,
    MsalModule.forRoot(
      new PublicClientApplication({
        auth: {
          clientId: 'bbabab-eefd-baba-baba-79fe140owos', // Application (client) ID from the app registration
          authority: 'https://login.microsoftonline.com/asbsbs-f9c9-absbs-86cb-absbs', // The Azure cloud instance and the app's sign-in audience (tenant ID, common, organizations, or consumers)
          redirectUri: 'https://localhost:4200/', // This is your redirect URI
          postLogoutRedirectUri: 'https://localhost:4200/',
        },
        cache: {
          cacheLocation: "localStorage",
          storeAuthStateInCookie: isIE, // Set to true for Internet Explorer 11
        },
      }),
      null,
      null
    ),
  ],

  providers: [],
  bootstrap: [AppComponent,MsalRedirectComponent]
})
export class AppModule { }

Step 2: Override the Login Form Component to create a button for Login and Logic for Authentication.

Create a new Component in your app with the module, scss, and HTML files and add the below code in respective files

Below is what my local folder structure looks like.

– app

– Spartacus

 

– features

 

– login-sso (Create new folder)

 

– login-form

 

– login-form.component.ts

 

– login-form.component.html

 

– login-form. component.scss

 

– login-form.module.ts

Below is the Code Snippet for login-form.module.ts

/*
 * SPDX-FileCopyrightText: 2023 SAP Spartacus team <spartacus-team@sap.com>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import {
  AuthService,
  CmsConfig,
  ConfigModule,
  GlobalMessageService,
  I18nModule,
  NotAuthGuard,
  provideDefaultConfig,
  UrlModule,
  WindowRef,
} from '@spartacus/core';
import {
  FormErrorsModule,
  SpinnerModule,
  PasswordVisibilityToggleModule,
} from '@spartacus/storefront';
import { LoginFormComponentService } from '@spartacus/user/account/components';
import { CustomLoginFormComponent } from './login-form.component';


@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    RouterModule,
    UrlModule,
    I18nModule,
    FormErrorsModule,
    SpinnerModule,
    PasswordVisibilityToggleModule,
    ConfigModule.withConfig({
      cmsComponents: {
        ReturningCustomerLoginComponent: {
          component: CustomLoginFormComponent,
          guards: [NotAuthGuard],
          providers: [
            {
              provide: LoginFormComponentService,
              useClass: LoginFormComponentService,
              deps: [AuthService, GlobalMessageService, WindowRef],
            },
          ],
        },
      },
    } as CmsConfig),
  ],
  providers: [],
  declarations: [CustomLoginFormComponent],
})
export class CustomLoginFormModule {}

Below is the code snippet for login-form-component.ts

/*
 * SPDX-FileCopyrightText: 2023 SAP Spartacus team <spartacus-team@sap.com>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { MsalBroadcastService, MsalService } from '@azure/msal-angular';
import {
  AuthenticationResult,
  EventMessage,
  EventType,
} from '@azure/msal-browser';
import {
  catchError,
  filter,
  map,
  switchMap,
  take,
  takeUntil,
  tap,
} from 'rxjs/operators';
import {
  BehaviorSubject,
  combineLatest,
  EMPTY,
  Observable,
  of,
  Subject,
  Subscription,
  throwError,
} from 'rxjs';
import { HttpClient, HttpParams } from '@angular/common/http';
import {
  AuthActions,
  AuthConfigService,
  AuthRedirectService,
  AuthService,
  AuthStorageService,
  AuthToken,
  BaseSiteService,
  GlobalMessageService,
  GlobalMessageType,
  OCC_USER_ID_CURRENT,
  User,
  UserIdService,
} from '@spartacus/core';
import { Store } from '@ngrx/store';
import { UserAccountFacade } from '@spartacus/user/account/root';

export interface MSALAuthenticationResponse {
  eventType: string;
  payload: MSALResponsePayload;
}

export interface MSALResponsePayload {
  accessToken: string;
  expiresOn: Date;
  idToken: string;
  account: MSALAccountInformation;
}

export interface MSALAccountInformation {
  name: string;
  username: string;
}

@Component({
  selector: 'cx-login-form',
  templateUrl: './login-form.component.html',
  styleUrls: ['./login-form.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomLoginFormComponent implements OnInit {
  title = 'msal-spartacus';
  isIframe = false;
  loginDisplay = new BehaviorSubject(false);
  loader$ = new BehaviorSubject(false);
  data: MSALAuthenticationResponse;
  private readonly _destroying$ = new Subject<void>();
  protected subscription: Subscription = new Subscription();
  user$: Observable<User | undefined>;

  constructor(
    private msalService: MsalService,
    private msalBroadcastService: MsalBroadcastService,
    protected http: HttpClient,
    protected authConfigService: AuthConfigService,
    protected store: Store,
    protected authStorageService: AuthStorageService,
    protected userIdService: UserIdService,
    protected globalMessageService: GlobalMessageService,
    protected authRedirectService: AuthRedirectService,
    protected baseSiteService: BaseSiteService,
    private auth: AuthService,
    private userAccount: UserAccountFacade
  ) {}

  ngOnInit() {
    this.subscription.add(
      combineLatest([
        this.baseSiteService.getActive(),
        this.msalBroadcastService.msalSubject$.pipe(
          tap((msg: EventMessage) => {
            console.log('Events fired from MSAL ... ' + JSON.stringify(msg));
          }),
          filter(
            (msg: EventMessage) =>
              msg.eventType === EventType.LOGIN_SUCCESS ||
              msg.eventType === EventType.ACQUIRE_TOKEN_SUCCESS ||
              msg.eventType === EventType.SSO_SILENT_SUCCESS
          )
        ),
      ])
        .pipe(takeUntil(this._destroying$))
        .subscribe(([baseSite, result]) => {
          this.setLoginDisplay();
          this.loader$.next(true);
          const event = result as EventMessage;
          const payload = event.payload as AuthenticationResult;
          this.msalService.instance.setActiveAccount(payload.account);
          this.loadTokenUsingCustomFlow(payload.idToken, baseSite).subscribe(
            (res) => {
              this.loginWithToken(res);
            }
          );
        })
    );

    // We can also handle Any Issue in Login with MSAL By Subscribing to MsalBroadcastService and its event EventType

    this.isIframe = window !== window.parent && !window.opener;
    this.setLoginDisplay();
  }

  login() {
    this.msalService.loginRedirect().subscribe({
      next: (result) => {
        this.setLoginDisplay();
      },
      error: (error) => console.log(error),
    });
  }

  setLoginDisplay() {
    console.log(
      'MSAL logged In? :' + this.msalService.instance.getAllAccounts().length
    );
    this.loginDisplay.next(
      this.msalService.instance.getAllAccounts().length > 0
    );
    this.user$ = this.auth.isUserLoggedIn().pipe(
      switchMap((isUserLoggedIn) => {
        if (isUserLoggedIn) {
          return this.userAccount.get();
        } else {
          return of(undefined);
        }
      })
    );
  }

  loadTokenUsingCustomFlow(
    UID: string,
    baseSite: string
   Observable<Partial<AuthToken> & { expires_in?: number }> {
    const url = this.authConfigService.getTokenEndpoint();
    const params = new HttpParams()
      .set('client_id', this.authConfigService.getClientId())
      .set('client_secret', this.authConfigService.getClientSecret())
      .set('grant_type', 'custom')
      .set('UID', encodeURIComponent(UID))
      .set('baseSite', encodeURIComponent(baseSite));

    return this.http
      .post<Partial<AuthToken> & { expires_in?: number }>(url, params)
      .pipe(catchError((error) => this.handleAuthError(error)));
  }
  handleAuthError(error: any): any {
    this.globalMessageService.add(
      error.message ? error.message : { key: 'httpHandlers.unknownIdentifier' },
      GlobalMessageType.MSG_TYPE_ERROR
    );
    this.setLoginDisplay();
    return of();
  }

  /**
   * Transform and store the token received from custom flow to library format and login user.
   *
   * @param token
   */
  loginWithToken(token: Partial<AuthToken> & { expires_in?: number }): void {
    let stream$ = of(true);
    stream$.pipe(take(1)).subscribe((canLogin) => {
      if (canLogin) {
        // Code mostly based on auth lib we use and the way it handles token properties
        this.setTokenData(token);

        // OCC specific code
        this.userIdService.setUserId(OCC_USER_ID_CURRENT);

        this.store.dispatch(new AuthActions.Login());

        // Remove any global errors and redirect user on successful login
        this.globalMessageService.remove(GlobalMessageType.MSG_TYPE_ERROR);
        this.loader$.next(false);
        this.authRedirectService.redirect();
      }
    });
  }

  protected setTokenData(token: any): void {
    this.authStorageService.setItem('access_token', token.access_token);

    if (token.granted_scopes && Array.isArray(token.granted_scopes)) {
      this.authStorageService.setItem(
        'granted_scopes',
        JSON.stringify(token.granted_scopes)
      );
    }

    this.authStorageService.setItem('access_token_stored_at', '' + Date.now());

    if (token.expires_in) {
      const expiresInMilliseconds = token.expires_in * 1000;
      const now = new Date();
      const expiresAt = now.getTime() + expiresInMilliseconds;
      this.authStorageService.setItem('expires_at', '' + expiresAt);
    }

    if (token.refresh_token) {
      this.authStorageService.setItem('refresh_token', token.refresh_token);
    }
  }

  ngOnDestroy(): void {
    this._destroying$.next(undefined);
    this._destroying$.complete();
  }
}

Below is the code snippet for login-form.component.html 

<div *ngIf="loader$ | async">
  <div id="cover-spin">
    <h3 class="msg">Please Wait Loading...</h3>
  </div>
</div>
<div class="container">
  <div class="row mb-3">
    <button
      (click)="login()"
      *ngIf="!(user$ | async) && !(loginDisplay | async)"
      class="btn btn-block btn-secondary btn-register"
    >
      Log In
    </button>
  </div>
</div>

<div class="container">
  <!--This is to avoid reload during acquireTokenSilent() because of hidden iframe -->
  <router-outlet *ngIf="!isIframe"></router-outlet>
</div>

Below is the code snippet for the login-form.component.scss

#cover-spin {
    position:fixed;
    width:100%;
    left:0;right:0;top:0;bottom:0;
    background-color: rgba(255,255,255,0.7);
    z-index:9999;
}

@-webkit-keyframes spin {
	from {-webkit-transform:rotate(0deg);}
	to {-webkit-transform:rotate(360deg);}
}

@keyframes spin {
	from {transform:rotate(0deg);}
	to {transform:rotate(360deg);}
}

#cover-spin::after {
    content:'';
    display:block;
    position:absolute;
    left:48%;top:40%;
    width:100px;height:100px;
    border-style:solid;
    border-color:black;
    border-top-color:transparent;
    border-width: 4px;
    border-radius:50%;
    -webkit-animation: spin .8s linear infinite;
    animation: spin .8s linear infinite;
}

.msg{
    margin-left: 46%;
    margin-right: 35%;
    margin-top: 18%;
    margin-bottom: 50%;
    position: fixed;
}

Step 3: After the above changes, register the CustomLoginFormModule into the app.module.ts. If not done you cannot see the changes.

Step 4: Now let’s handle the Logout scenario in the Composable Storefront. In order to achieve this, you need to override the AuthHttpHeaderService and AuthService. Create a new folder “service” under the login-sso folder and create two new files as “msal-auth-http-header.service.ts” and “meal-auth-service.ts”.

Below is the code snippet for both files.

/*
 * SPDX-FileCopyrightText: 2023 SAP Spartacus team <spartacus-team@sap.com>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

import { HttpEvent, HttpHandler, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import {
  AuthHttpHeaderService,
  AuthRedirectService,
  AuthService,
  AuthStorageService,
  AuthToken,
  GlobalMessageService,
  OAuthLibWrapperService,
  OccEndpointsService,
  RoutingService,
} from '@spartacus/core';
import { Observable, EMPTY, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { MSALSpaAuthService } from './msal-auth-service';

/**
 * Overrides `AuthHttpHeaderService` to handle asm calls as well (not only OCC)
 * in cases of normal user session and on customer emulation.
 */
@Injectable({
  providedIn: 'root',
})
export class MSALAuthHttpHeaderService extends AuthHttpHeaderService {
  constructor(
    protected msalAuthServide: MSALSpaAuthService,
    protected override authService: AuthService,
    protected override authStorageService: AuthStorageService,
    protected override oAuthLibWrapperService: OAuthLibWrapperService,
    protected override routingService: RoutingService,
    protected override globalMessageService: GlobalMessageService,
    protected occEndpointsService: OccEndpointsService,
    protected override authRedirectService: AuthRedirectService
  ) {
    super(
      authService,
      authStorageService,
      oAuthLibWrapperService,
      routingService,
      occEndpointsService,
      globalMessageService,
      authRedirectService
    );
  }

  /**
   * Refreshes access_token and then retries the call with the new token.
   */
  public override handleExpiredAccessToken(
    request: HttpRequest<any>,
    next: HttpHandler,
    initialToken: AuthToken | undefined
   Observable<HttpEvent<AuthToken>> {
    this.handleExpiredRefreshToken();
    return of<HttpEvent<any>>();
  }

  /**
   * Logout user, redirected to login page and informs about expired session.
   */
  public override handleExpiredRefreshToken(): void {
    // There might be 2 cases:
    // 1. when user is already on some page (router is stable) and performs an UI action
    // that triggers http call (i.e. button click to save data in backend)
    // 2. when user is navigating to some page and a route guard triggers the http call
    // (i.e. guard loading cms page data)
    //
    // In the second case, we want to remember the anticipated url before we navigate to
    // the login page, so we can redirect back to that URL after user authenticates.
    this.authRedirectService.saveCurrentNavigationUrl();

    // Logout user
    // TODO(#9638): Use logout route when it will support passing redirect url
    this.msalAuthServide.coreLogout();
  }
}

 

/*
 * SPDX-FileCopyrightText: 2023 SAP Spartacus team <spartacus-team@sap.com>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

import { Injectable } from '@angular/core';
import { MsalService } from '@azure/msal-angular';
import { EndSessionRequest } from '@azure/msal-browser';
import { Store } from '@ngrx/store';
import { AsmAuthStorageService } from '@spartacus/asm/root';
import { environment } from '../../../../../environments/environment';
import {
  AuthMultisiteIsolationService,
  AuthRedirectService,
  AuthService,
  GlobalMessageService,
  OAuthLibWrapperService,
  RoutingService,
  StateWithClientAuth,
  UserIdService,
} from '@spartacus/core';

/**
 * Version of AuthService that is working for both user na CS agent.
 * Overrides AuthService when ASM module is enabled.
 */
@Injectable({
  providedIn: 'root',
})
export class MSALSpaAuthService extends AuthService {
  constructor(
    protected msalService: MsalService,
    protected override store: Store<StateWithClientAuth>,
    protected override userIdService: UserIdService,
    protected override oAuthLibWrapperService: OAuthLibWrapperService,
    protected override authStorageService: AsmAuthStorageService,
    protected override authRedirectService: AuthRedirectService,
    protected globalMessageService: GlobalMessageService,
    protected override routingService: RoutingService,
    protected override authMultisiteIsolationService?: AuthMultisiteIsolationService
  ) {
    super(
      store,
      userIdService,
      oAuthLibWrapperService,
      authStorageService,
      authRedirectService,
      routingService,
      authMultisiteIsolationService
    );
  }

  /**
   * Revokes tokens and clears state for logged user (tokens, userId).
   * To perform logout it is best to use `logout` method. Use this method with caution.
   */
  override coreLogout(): Promise<void> {
    return super.coreLogout().finally(() => {
      this.MSALlogout();
    });
  }

  MSALlogout() {
    const session: EndSessionRequest = {
      authority: environment.msal.auth.authority,
      onRedirectNavigate: (url) => {
        // The value of 'url' is the URL that MSAL would redirect the user to.
        console.log('Redirect URL is...  ' + url);
        return true;
      },
    };
    this.msalService.logout(session);
  }
}

Step 5:  Register both service class in the app.module.ts providers array. Refer Below code snippet.

providers: [
    {
      provide: AuthHttpHeaderService,
      useExisting: MSALAuthHttpHeaderService,
    },
    {
      provide: AuthService,
      useExisting: MSALSpaAuthService,
    },
  ],

 

SAP Commerce Implementation :  

Once you have completed the changes from Spartacus, it’s time to do some changes in the SAP Commerce to implement Custom Token Generator.

Step 1: Create a new class MSALCustomTokenGenerator which extends AbstractTokenGranter and register its bean in the new extension “spasso”

package com.spa.sso.token;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.hybris.platform.basecommerce.model.site.BaseSiteModel;
import de.hybris.platform.commerceservices.customer.CustomerAccountService;
import de.hybris.platform.commerceservices.customer.DuplicateUidException;
import de.hybris.platform.core.model.user.CustomerModel;
import de.hybris.platform.core.model.user.UserModel;
import de.hybris.platform.servicelayer.exceptions.UnknownIdentifierException;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.servicelayer.user.UserService;
import de.hybris.platform.site.BaseSiteService;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import com.spa.sso.data.MSARequestPayload;
import com.spa.sso.data.MSALAuthenticationPayload;
import javax.annotation.Resource;
import javax.ws.rs.BadRequestException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;

public class MSALCustomTokenGenerator extends AbstractTokenGranter
{

    public static final String UTF8 = "UTF-8";

    @Resource
    private UserService userService;

    private UserDetailsService userDetailsService;
    private BaseSiteService baseSiteService;

    private ModelService modelService;

    private CustomerAccountService customerAccountService;


    protected MSALCustomTokenGenerator(final AuthorizationServerTokenServices tokenServices,
                                       final ClientDetailsService clientDetailsService, final OAuth2RequestFactory requestFactory,
                                       final BaseSiteService baseSiteService, final UserDetailsService userDetailsService,
                                       final ModelService modelService,final CustomerAccountService customerAccountService)
    {
        super(tokenServices, clientDetailsService,requestFactory,"custom");
        this.userDetailsService = userDetailsService;
        this.baseSiteService = baseSiteService;
        this.modelService = modelService;
        this.customerAccountService = customerAccountService;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(final ClientDetails client,
                                                           final TokenRequest tokenRequest)
    {

        final MSARequestPayload jsInfo = initializeMSALInfoObject(tokenRequest.getRequestParameters());
        final String uid = jsInfo.getUID();
        final String baseSite = jsInfo.getBaseSite();
        final BaseSiteModel currentBaseSite = configureBaseSiteInSession(baseSite);

        if (currentBaseSite != null && Objects.nonNull(uid) )
        {
            final String[] chunks = uid.split("\.");
            final Base64.Decoder decoder = Base64.getUrlDecoder();

            //final String header = new String(decoder.decode(chunks[0]));
            //final String signature = chunks[2];
            final MSALAuthenticationPayload payload = initializePayload(new String(decoder.decode(chunks[1])));
            UserModel user;
            try
            {
                user = userService.getUserForUID(payload.getUserName());
            }
            catch (UnknownIdentifierException ex)
            {
                user =  createUser(payload);
            }
            final UserDetails loadedUser = userDetailsService.loadUserByUsername(payload.getUserName());
            final Authentication userAuth = new UsernamePasswordAuthenticationToken(user.getUid(), null,
                    loadedUser.getAuthorities());
            final OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);

            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        }
        else
        {
            throw new InvalidRequestException("Invalid request received");
        }
    }

    private CustomerModel createUser(MSALAuthenticationPayload payload) {
        final CustomerModel customer = modelService.create(CustomerModel.class);
        customer.setName(payload.getName());
        customer.setUid(payload.getUserName());
        customer.setOriginalUid(payload.getUserName());
        try {
            customerAccountService.register(customer, null);
        } catch (DuplicateUidException e) {
            throw new BadRequestException("Error Occurred");
        }
        return customer;
    }

    private BaseSiteModel configureBaseSiteInSession(final String baseSite)
    {
        final BaseSiteModel currentBaseSite = baseSiteService.getBaseSiteForUID(baseSite);
        baseSiteService.setCurrentBaseSite(currentBaseSite, false);
        return currentBaseSite;
    }

    private MSALAuthenticationPayload initializePayload(final String json)
    {

        final var mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
        MSALAuthenticationPayload jsInfo = null;
        try {
            jsInfo =  mapper.readValue(json, MSALAuthenticationPayload.class);
        } catch (IllegalArgumentException | JsonProcessingException e)
        {
            throw new BadRequestException("Invalid request received");
        }
        return jsInfo;
    }

    private MSARequestPayload initializeMSALInfoObject(final Map<String, String> parameters)
    {

        final var mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
        MSARequestPayload jsInfo = null;

        try
        {
            jsInfo = mapper.convertValue(parameters, MSARequestPayload.class);
            // Decode the parameters
            if (jsInfo.getUID() != null && StringUtils.isNotBlank(jsInfo.getUID())) {
                jsInfo.setUID(URLDecoder.decode(jsInfo.getUID(), UTF8));
            }
        }
        catch (UnsupportedEncodingException | IllegalArgumentException e)
        {
            throw new BadRequestException("Invalid request received");
        }

        return jsInfo;
    }

}

Register above class in the spasso-spring.xml

 <alias name="msalCustomTokenGranter" alias="customTokenGranter"/>
    <bean id="msalCustomTokenGranter" class="com.spa.sso.token.MSALCustomTokenGenerator"  >
        <constructor-arg name="tokenServices" ref="oauthTokenServices" />
        <constructor-arg name="clientDetailsService" ref="oauthClientDetails" />
        <constructor-arg name="requestFactory" ref="oAuth2RequestFactory" />
        <constructor-arg name="baseSiteService" ref="baseSiteService" />
        <constructor-arg name="userDetailsService" ref="wsUserDetailsService" />
        <constructor-arg name="baseSiteService" ref="baseSiteService" />
        <constructor-arg name="customerAccountService" ref="customerAccountService"/>
        <constructor-arg name="modelService" ref="modelService"/>
    </bean>

Register the DTO in the “spasso-beans.xml”

	<bean class="com.spa.sso.data.MSALAuthenticationPayload">

		<import type="com.fasterxml.jackson.annotation.JsonIgnoreProperties" />
		<import type="com.fasterxml.jackson.annotation.JsonProperty" />

		<annotations>@JsonIgnoreProperties(ignoreUnknown = true)</annotations>
		<property name="aud" type="java.lang.String" >
			<annotations>@JsonProperty("aud")</annotations>
		</property>
		<property name="name" type="java.lang.String" >
			<annotations>@JsonProperty("name")</annotations>
		</property>
		<property name="userName" type="java.lang.String" >
			<annotations>@JsonProperty("preferred_username")</annotations>
		</property>
	</bean>

	<bean class="com.spa.sso.data.MSARequestPayload">

		<import type="com.fasterxml.jackson.annotation.JsonIgnoreProperties" />
		<import type="com.fasterxml.jackson.annotation.JsonProperty" />

		<annotations>@JsonIgnoreProperties(ignoreUnknown = true)</annotations>

		<property name="UID" type="java.lang.String" >
			<annotations>@JsonProperty("UID")</annotations>
		</property>
		<property name="baseSite" type="java.lang.String" />
	</bean>

Step 2: Run Ant Clean all and resolve if there are any build errors and start the Hybris Server.

Update the mobile_android OAuth client to support the custom authorization grant type, and remove the refresh_token grant type. The following ImpEx can be used to update the grant types:

INSERT_UPDATE OAuthClientDetails ; clientId[unique = true] ; resourceIds ; scope ; authorizedGrantTypes                                  ; authorities ; clientSecret ; registeredRedirectUri                                     
                                  ; mobile_android          ; hybris      ; basic ; authorization_code,password,client_credentials,custom ; ROLE_CLIENT ; secret       ; http://localhost:9001/authorizationserver/oauth2_callback ;  

Note: Refresh tokens are not supported. This ensures that the token from Commerce Cloud and the Microsoft account session is maintained for the same duration of time.

Once the above changes are done, Now it’s ready for testing. Check and resolve any errors in the Composable storefront and in the SAP Commerce.

TESTING :

1. Navigate to Electronics Storefront

2. Click on Sign In

 

Login Page

3. Click on Log In, User will be redirected to Microsoft Login Page as below, and enter the email address. NOTE: You have to create a user in the Azure portal before signing In.

 

Microsoft Login Page

4.  In the Next step you will be prompted to set up the Microsoft Authenticator app.

Click  on   “Ask Later”

 

Authorization Setup

5. User will now be redirected to the home page as Logged In User. If the user is new, then it will create the user in SAP Commerce. You can validate in Backoffice.

 

Logged In User

Here is the video demo for the same.

https://video.sap.com/media/t/1_w5cf7hgl

Hurray !!. You have now successfully implemented the SSO functionality in the Composable storefront.

Make sure you give this post a like and follow my blog if you find it helpful.

NOTE:  You can still improve the approach and security of handling the access/JWT token. This post describes the basic approach to implementing the SSO functionality. Please consider all the edge cases before using them in production.

Sara Sampaio

Sara Sampaio

Author Since: March 10, 2022

0 0 votes
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x