AzureAD / microsoft-authentication-library-for-js

Microsoft Authentication Library (MSAL) for JS
http://aka.ms/aadv2
MIT License
3.65k stars 2.65k forks source link

How to use single sign on with this package #492

Closed vipswelt closed 5 years ago

vipswelt commented 5 years ago

I have two apps one in .net and another in angular, i am not able use single sign on with this package as it always promp for user name password page. How can i achieve that. Please reply with further information to acieve this. If it is not possible please reply me as well

Its really urgent for me.

navyasric commented 5 years ago

@vipswelt Please take a look at this Wiki topic on SSO between apps using MSAL.js and let us know if it resolves your issue. https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/Sso#sso-between-apps

vipswelt commented 5 years ago

@navyasric not resolved

getting error while already autheticated with with my .net app http://localhost:4200/auth-callback#error=interaction_required&error_description=AADB2C90077%3a+User+does+not+have+an+existing+session+and+request+prompt+parameter+has+a+value+of+%27None%27.%0d%0aCorrelation+ID%3a+d60f7e7e-ed42-40ab-a334-d7ef620db4b2%0d%0aTimestamp%3a+2018-11-22+07%3a37%3a01Z%0d%0a&state=9a1e5b76-f589-41c2-a3af-5f61fcd6acef

vipswelt commented 5 years ago

No one is replying, no one has a solution? please reply its really urgent for me

navyasric commented 5 years ago

@vipswelt Can you provide the exact steps to reproduce, library version , browser details, etc as per the issue template? That will help us help you better. Are both your apps(.net and angular) running in the same browser? Also, as per the wiki doc linked above, did you pass a login_hint of the user and then call acquireTokenSilent?

vipswelt commented 5 years ago

Hi @navyasric, Yes, both apps running on the same browser: Google Chrome (Version 70.0.3538.110 (Official Build) (64-bit)). But both apps are not able to retrieve the session from another logged in app. I can't use login_hint because I don't have any detail for any user going to be logged in. I am attaching a code snippet with this reply please have a quick look over it

### For Angular App (I am using @azure/msal-angular package in angular 6)

Config:

`{
    clientID: '4fe0a851-aec1-4241-8e46-fe13056f8d2e',
    authority: 'https://login.microsoftonline.com/tfp/******.onmicrosoft.com/b2c_1_susi/v2.0/.well-known/openid-configuration',
    tokenReceivedCallback: true,
    validateAuthority: false,
    cacheLocation: 'localStorage',
    redirectUri: 'http://localhost:4200/auth-callback',
    postLogoutRedirectUri: 'http://localhost:54503/',
    logger: loggerCallback,
    // loadFrameTimeout: number;
    navigateToLoginRequestUrl: false,
    popUp: false,
    consentScopes:
      [
        'https://******.onmicrosoft.com/finreg-api/read',
        'https://******.onmicrosoft.com/finreg-api/write',
        'https://******.onmicrosoft.com/finreg-api/profile',
        'https://******.onmicrosoft.com/finreg-api/offline_access',
        'https://******.onmicrosoft.com/finreg-api/open_id',
        'https://******.onmicrosoft.com/finreg-api/graph',
        'https://******.onmicrosoft.com/finreg-api/user_impersonation'
      ],
    // isAngular: true,
    unprotectedResources: ['https://www.microsoft.com/en-us/'],
    protectedResourceMap: protectedResourceMap,
    // extraQueryParameters: '';
    correlationId: '1234',
    level: LogLevel.Info,
    piiLoggingEnabled: true
  }`

app.module.ts:

`imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    ReactiveFormsModule,
    MatAutocompleteModule,
    MatInputModule,
    RouterModule.forRoot(AppRoutes, { useHash: false }),
    HttpClientModule,
    MomentModule,
    ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
    DistributedUIModule,

    **MsalModule.forRoot(environment.config.msal_config),**
  ],
  providers: [
    ...MAIN_SERVICES,
    **{
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor,
      multi: true
    },**
    { provide: LocationStrategy, useClass: PathLocationStrategy },
    { provide: CookieService, useFactory: cookieServiceFactory }
  ]`

AuthGaurd.ts: canActivate(): boolean { if (this._msalService._oauthData.isAuthenticated) { return true; } // tslint:disable-next-line:max-line-length this._msalService.loginRedirect(environment.config.msal_config.consentScopes); // , '&login_hint=weltofvips@gmail.com&domain_hint=organizations'); // this._msalService.loginRedirect(environment.config.msal_config.consentScopes); // this._msalService.acquireTokenSilent([environment.config.msal_config.authority]); // tslint:disable-next-line:max-line-length // this._msalService.loginRedirect(environment.config.msal_config.consentScopes, '&prompt=none&organizations&login_hint=myemail@domain.com'); return false; }

### For .net MVC app:

web.config: `

<add key="ida:ClientSecret" value="0n50*************.g1C|RS" />
<add key="ida:AadInstance" value="https://login.microsoftonline.com/tfp/{0}/{1}/v2.0/.well-known/openid-configuration" />
<add key="ida:RedirectUri" value="http://localhost:54503" />
<add key="ida:SignUpSignInPolicyId" value="b2c_1_susi" />
<add key="ida:EditProfilePolicyId" value="b2c_1_edit_profile" />
<add key="ida:ResetPasswordPolicyId" value="b2c_1_reset" />
<!-- Uncomment the localhost url if you want to run the API locally -->
<add key="api:TaskServiceUrl" value="https://********.azurewebsites.net/" />
<!-- The following settings is used for requesting access tokens -->
<add key="api:ApiIdentifier" value="https://********.onmicrosoft.com/HubApi/" />
<add key="api:ReadScope" value="read" />
<add key="api:WriteScope" value="write" />`

Startup.Auth.cs:

`public partial class Startup
    {
        // App config settings
        public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];
        public static string ClientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];
        public static string AadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
        public static string Tenant = ConfigurationManager.AppSettings["ida:Tenant"];
        public static string RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
        public static string SignUpRedirectUri = ConfigurationManager.AppSettings["ida:SignUpRedirectUri"];

        // B2C policy identifiers
        public static string SignUpSignInPolicyId = ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];
        public static string EditProfilePolicyId = ConfigurationManager.AppSettings["ida:EditProfilePolicyId"];
        public static string ResetPasswordPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];

        public static string DefaultPolicy = SignUpSignInPolicyId;

        // API Scopes
        public static string ApiIdentifier = ConfigurationManager.AppSettings["api:ApiIdentifier"];
        public static string ReadTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:ReadScope"];
        public static string WriteTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:WriteScope"];
        public static string OpenIdScope = ApiIdentifier + OpenIdConnectScope.OpenId;
        public static string ProfileScope = ApiIdentifier + "profile";
        public static string OfflineAccessScope = ApiIdentifier + OpenIdConnectScope.OfflineAccess;
        public static string UserImpersonationScope = ApiIdentifier + OpenIdConnectScope.UserImpersonation;
        public static string[] Scopes = new string[] { ReadTasksScope, WriteTasksScope, OpenIdScope, ProfileScope, OfflineAccessScope, UserImpersonationScope };

        // OWIN auth middleware constants
        public const string ObjectIdElement = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";

        // Authorities
        public static string Authority = String.Format(AadInstance, Tenant, DefaultPolicy);

        public static GraphServiceClient graphClient = null;

        readonly IAdminService _adminService = new AdminService();
        /*
        * Configure the OWIN middleware 
        */
        public void ConfigureAuth(IAppBuilder app)
        {
            // Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    // Generate the metadata address using the tenant and policy information
                    MetadataAddress = String.Format(AadInstance, Tenant, DefaultPolicy),

                    // These are standard OpenID Connect parameters, with values pulled from web.config
                    ClientId = ClientId,
                    RedirectUri = RedirectUri,
                    PostLogoutRedirectUri = RedirectUri,

                    // Specify the callbacks for each type of notifications
                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        RedirectToIdentityProvider = OnRedirectToIdentityProvider,
                        AuthorizationCodeReceived = OnAuthorizationCodeReceived,
                        AuthenticationFailed = OnAuthenticationFailed
                    },

                    // Specify the claim type that specifies the Name property.
                    TokenValidationParameters = new TokenValidationParameters
                    {
                        NameClaimType = "name"
                    },

                    // Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
                    Scope = $"openid profile offline_access {ReadTasksScope} {WriteTasksScope}"
                }
            );
        }

        /*
         *  On each call to Azure AD B2C, check if a policy (e.g. the profile edit or password reset policy) has been specified in the OWIN context.
         *  If so, use that policy when making the call. Also, don't request a code (since it won't be needed).
         */
        private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
        {
            var policy = notification.OwinContext.Get<string>("Policy");

            if (!string.IsNullOrEmpty(policy) && !policy.Equals(DefaultPolicy))
            {
                notification.ProtocolMessage.Scope = OpenIdConnectScope.OpenIdProfile + " " + OpenIdConnectScope.OfflineAccess + " " + OpenIdConnectScope.UserImpersonation;
                notification.ProtocolMessage.ResponseType = OpenIdConnectResponseType.Code;
                notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(DefaultPolicy.ToLower(), policy.ToLower());
            }

            return Task.FromResult(0);
        }

        /*
         * Catch any failures received by the authentication middleware and handle appropriately
         */
        private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
        {
            notification.HandleResponse();

            // Handle the error code that Azure AD B2C throws when trying to reset a password from the login page 
            // because password reset is not supported by a "sign-up or sign-in policy"
            if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
            {
                // If the user clicked the reset password link, redirect to the reset password route
                notification.Response.Redirect("/Account/ResetPassword");
            }
            else if (notification.Exception.Message == "access_denied")
            {
                notification.Response.Redirect("/");
            }
            else
            {
                notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
            }

            return Task.FromResult(0);
        }

        /*
         * Callback function when an authorization code is received 
         */
        private Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
        {
            // Extract the code from the response notification
            var code = notification.Code;

            string signedInUserID = notification.AuthenticationTicket.Identity.FindFirst(System.IdentityModel.Claims.ClaimTypes.NameIdentifier).Value;
            TokenCache userTokenCache = new MSALSessionCache(signedInUserID, notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase).GetMsalCacheInstance();
            ConfidentialClientApplication cca = new ConfidentialClientApplication(ClientId, Authority, RedirectUri, new ClientCredential(ClientSecret), userTokenCache, null);

            try
            {
                AuthenticationResult result = cca.AcquireTokenByAuthorizationCodeAsync(code, Scopes).Result;
                string token = result.AccessToken;

                if (notification.AuthenticationTicket.Identity.FindFirst("newUser") != null)
                {
                    var isNewUser = Convert.ToBoolean(notification.AuthenticationTicket.Identity.FindFirst("newUser").Value);
                    if (isNewUser)
                    {
                        SignUp(notification);
                        return Task.FromResult(0);
                        //notification.OwinContext.Response.Redirect("/Home/SignUp");
                        //notification.Options.RedirectUri = SignUpRedirectUri;
                        //notification.HandleResponse();
                    }
                }
            }
            catch (Exception ex)
            {
                //TODO: Handle
                // throw;
            }
            return Task.FromResult(0);
        }

        public void SignUp(AuthorizationCodeReceivedNotification notification)
        {
            string userId = notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;

            string fullName = notification.AuthenticationTicket.Identity.Name;

            string firstName = (notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.GivenName) != null) ? notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.GivenName).Value : string.IsNullOrEmpty(fullName) ? string.Empty : fullName.Split(' ')[0];

            string lastName = (notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Surname) != null) ? notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Surname).Value : fullName.Split(' ').Length<=1 ? string.Empty : fullName.Split(' ')[1];

            string email = (notification.AuthenticationTicket.Identity.FindFirst("emails") != null) ?notification.AuthenticationTicket.Identity.FindFirst("emails").Value : string.Empty;

            HubAccountDto account = new HubAccountDto();
            account.Title = fullName + "_" + DateTime.UtcNow.Ticks;
            account.Description = fullName + "_" + DateTime.UtcNow.Ticks;
            account.CreatedOn = DateTime.UtcNow;
            account.UpdatedOn = DateTime.UtcNow;
            account.IsActive = true;
            _adminService.CreateAccount(account);

            HubUserDto user = new HubUserDto();
            user.AccountId = account.Id;
            user.Email = email;
            user.ExternalUserId = userId;
            user.FirstName = firstName;
            user.LastName = lastName;
            user.ImageUrl = null;
            _adminService.CreateUser(user);
        }
    }`

AccountController.cs:

'public class AccountController : Controller
    {
        /*
         *  Called when requesting to sign up or sign in
         */
        public void SignUpSignIn()
        {
            // Use the default policy to process the sign up / sign in flow
            if (!Request.IsAuthenticated)
            {
                HttpContext.GetOwinContext().Authentication.Challenge();
                return;
            }

            Response.Redirect("/");
        }

        /*
         *  Called when requesting to edit a profile
         */
        public void EditProfile()
        {
            if (Request.IsAuthenticated)
            {
                // Let the middleware know you are trying to use the edit profile policy (see OnRedirectToIdentityProvider in Startup.Auth.cs)
                HttpContext.GetOwinContext().Set("Policy", Startup.EditProfilePolicyId);

                // Set the page to redirect to after editing the profile
                var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
                HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);

                return;
            }

            Response.Redirect("/");

        }

        /*
         *  Called when requesting to reset a password
         */
        public void ResetPassword()
        {
            // Let the middleware know you are trying to use the reset password policy (see OnRedirectToIdentityProvider in Startup.Auth.cs)
            HttpContext.GetOwinContext().Set("Policy", Startup.ResetPasswordPolicyId);

            // Set the page to redirect to after changing passwords
            var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
            HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);

            return;
        }

        /*
         *  Called when requesting to sign out
         */
        public void SignOut()
        {
            // To sign out the user, you should issue an OpenIDConnect sign out request.
            if (Request.IsAuthenticated)
            {
                IEnumerable<AuthenticationDescription> authTypes = HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes();
                HttpContext.GetOwinContext().Authentication.SignOut(authTypes.Select(t => t.AuthenticationType).ToArray());
                Request.GetOwinContext().Authentication.GetAuthenticationTypes();
            }
        }
    }
}'

and I have another concern that is: Now I am having three apps two on .Net and one in angular, for .Net MVC apps both apps sharing their session for login and for logout working fine locally but after publishing the code on azure Single Single Out is not working as well

Please get me to the resolution for both of my concerns ASAP, it is really urgent

vipswelt commented 5 years ago

Hi @navyasric, is there anything to resolve this issue?

navyasric commented 5 years ago

@vipswelt The msal-angular library cannot provide an single sign on experience without having access to an authenticated user session with AAD. You will need to provide a login hint and then call acquireTokenSilent for an API to avoid logging in again in the Angular app. You can get the login_hint from the idToken claims when user signs in to the 1st app. If you don't have any access to a sign in user's session or idToken, then you will have to prompt user to login in the Angular app before calling APIs.

navyasric commented 5 years ago

Closing this issue with the above guidance.