Authorization for the lazy. Our rake


    Hello! Recently, we solved the problem of authorizing users of a mobile application on our backend. So what, you ask, the task has already been solved a thousand times . In this article I will not tell a success story. I’d better tell you about the rake that we collected.


    A little about the project


    At 2GIS we make a cool and accurate directory of companies. To ensure the quality and relevance of data in 2GIS there are several internal systems. One of them is called YouLa - no, not the one where the ads are published. Our YouLa supports the process of reconciling data on the ground.


    Part of the system is the mobile app pedestrians walk with. Pedestrians are specialists who go around the whole city. The map is divided into different sections for verification.


    Look at what the territorial division of the Moscow region looks like. Different colors on the map indicate different destination areas.



    Pedestrians bring us information about organizations that cannot be called or have no website. For example, barbecue, stalls with vegetables. In addition, it happens that the organization has changed the phone and we can not get through there. In all the cases mentioned, our specialist comes to the organization and checks the information on the ground.


    Just a few days ago, we launched a new mobile application for which we wrote a backend for data synchronization.


    At our new backend we want to know what kind of user came to us.


    We have several requirements for authorization:


    - reliability and security,
    - authentication from various sources,
    - authentication of several types of Web, Mobile, API clients.


    Choosing an Authentication Method


    There are many different approaches to implementing authentication, each with its own pros and cons. Given that we have a large number of integration points,
    we decided not to reinvent the wheel and take an authentication and authorization provider based on OpenId Connect. For authorization on the backend we use JWT.


    Read more in the article “ Authentication and authorization in microservice applications ”.


    What is good about JWT and the OpenId Connect standard in Enterprise?


    Now, even within the framework of one company, systems are developed on different technology stacks and it is often difficult to make friends later on. Within one technology stack, you can also catch many strange and unexpected effects , to say nothing of the situation when you have several systems. For JWT and OpenId Connect, the list of supported clients and platforms is impressive .


    The scheme of operation of all components looks like this:


    The protocol supports dynamic connection of authentication providers. We examined two sources - Google+ and ADFS. But in the future, we would like to simply and quickly expand the product audience, for example, by connecting other companies to the system that could solve their problems in our system.


    Using JWT, you can easily organize authentication of variegated clients. Moreover, many cloud clients immediately offer a whole set of libraries that facilitate the integration of the provider in your application.


    Cloud solutions


    The first platform we decided to try was Auth0. The platform is very cool for both the developer and the administrator. It has detailed documentation, a beautiful and intuitive Web UI for setting all the parameters. In our Java / Kotlin application and backend, authentication was fastened in a couple of hours.


    The main advantages that we noted when working with the Auth0 platform:


    - Detailed documentation and an infinite number of code samples in common programming languages;
    - the ability to use for authentication not the web, but the native login form.


    In order to implement support for JWT authentication in the backend, it’s enough to write only a few lines (this code for different platforms will differ only in the parameters Authority and Audience), in some cases, you will also need to specify certificates to verify the signature of tokens:


    .NET Core backend code
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddMvc();
                // 1. Add Authentication Services
                services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                }).AddJwtBearer(options =>
                {
                    options.Authority = "https://devday2gis.auth0.com/";
                    options.Audience = "https://devday.api";
                });
            }
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                // 2. Add Authentication
                app.UseAuthentication();
                app.UseMvc();
            }

    In order to fasten authentication to the mobile phone - a few more lines:


    Mobile phone login code
     private void login() {
            token.setText("Not logged in");
            Auth0 auth0 = new Auth0(this);
            auth0.setOIDCConformant(true);
            WebAuthProvider.init(auth0)
                    .withScheme("demo")
                    .withAudience(String.format("https://%s/userinfo", getString(R.string.com_auth0_domain)))
                    .withScope("openid email profile")
                    .start(MainActivity.this, new AuthCallback() {
                        @Override
                        public void onSuccess(@NonNull final Credentials credentials) {
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    idToken = credentials.getIdToken();
                                    accessToken = credentials.getAccessToken();
                                    Log.d("id token", credentials.getIdToken());
                                    Log.d("access token", credentials.getAccessToken());
                                    token.setText("Logged in: " + credentials.getIdToken());
                                }
                            });
                        }
                        @Override
                        public void onFailure(@NonNull final Dialog dialog) {
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    dialog.show();
                                }
                            });
                        }
                        @Override
                        public void onFailure(final AuthenticationException exception) {
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    Toast.makeText(MainActivity.this, "Error: " + exception.getMessage(), Toast.LENGTH_SHORT).show();
                                }
                            });
                        }
                    });
        }

    As you can see from the example, after authentication, two tokens come to us + one more (RefreshToken) is not shown in the code.


    What are they needed for?


    IdToken - contains the credentials of the
    AccessToken user - for authorization on the
    RefreshToken API - for updating the AccessToken


    Backfill question: why are two Access and Refresh tokens necessary?


    Answer

    Consider two cases of key theft:


    1. The scoundrel stole only the AccessToken. Then it will be valid only until you use your RefreshToken.
    2. The scoundrel stole both tokens. Then, as soon as it uses RefreshToken, your tokens will cease to function and you will be logged out of the application. If you use your credentials, then the attacker's tokens will cease to act.
      Using two tokens limits the time that an attacker will have access to your APIs.

    The JWT-IdToken token itself looks like this:


    The mobile application receives information about the authenticated user from this token. Accordingly, we use IdToken to draw the name of the user and his avatar.


    AccessToken we attach to the header of requests:


    API call from Android application
    private void makeApiCall()
        {
            DevDayApi api = CreateApi("http://rnd-123.2gis.local/", accessToken);
            api.getUserProfile().enqueue(new Callback() {
                @Override
                public void onResponse(Call call, Response response) {
                    runOnUiThread(() -> {
                        if(response.body() != null)
                            apiAnswer.setText(response.body().Answer);
                    });
                }
                @Override
                public void onFailure(Call call, Throwable t) {
                    apiAnswer.setText(t.getMessage());
                }
            });
        }
        private DevDayApi CreateApi(String baseUrl, String authToken)
        {
            HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
            logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);
            OkHttpClient client = new OkHttpClient.Builder()
                    .addInterceptor(chain -> {
                        Request newRequest = chain.request().newBuilder()
                                .addHeader("Authorization", "Bearer " + authToken)//tokenProvider.getAuthToken())
                                .build();
                        return chain.proceed(newRequest);
                    })
                    .addInterceptor(logging)
                    .build();
            Retrofit retrofit = new Retrofit.Builder()
                    .client(client)
                    .baseUrl(baseUrl)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
            return retrofit.create(DevDayApi.class);
        }

    For web client authentication, it is also quite simple to do an interactive login through IdenitityProvider. Below is an example from the official documentation on how to fasten this to an Angular4 application.


    Web Application Backend Authentication
    import { Injectable } from '@angular/core';
    import { AUTH_CONFIG } from './auth0-variables';
    import { Router } from '@angular/router';
    import * as auth0 from 'auth0-js';
    @Injectable()
    export class AuthService {
      auth0 = new auth0.WebAuth({
        clientID: AUTH_CONFIG.clientID,
        domain: AUTH_CONFIG.domain,
        responseType: 'token id_token',
        audience: `https://${AUTH_CONFIG.domain}/userinfo`,
        redirectUri: AUTH_CONFIG.callbackURL,
        scope: 'openid'
      });
      constructor(public router: Router) {}
      public login(): void {
        this.auth0.authorize();
      }
      public handleAuthentication(): void {
        this.auth0.parseHash((err, authResult) => {
          if (authResult && authResult.accessToken && authResult.idToken) {
            this.setSession(authResult);
            this.router.navigate(['/home']);
          } else if (err) {
            this.router.navigate(['/home']);
            console.log(err);
            alert(`Error: ${err.error}. Check the console for further details.`);
          }
        });
      }
    }

    As you can see from the examples, no one was offended - the implementation for the customers is simple and straightforward.


    For complete happiness, we lacked user authentication through our local Active Directory.


    To configure synchronization between Auth0 and the local Active Directory, Auth0 provides a powershell script.



    Когда мы уже обрадовались, что всё отлично работает, и пошли к админам с просьбой настроить синхронизацию между нашим AD и Auth0, то получили отказ. Ребята сказали, что максимум, куда они готовы лить наши данные, — это Azure. Также на решение повлияло то, что у нас уже использовалась подписка Office 365 и часть учёток уже была залита в Azure.


    Окей, сказали мы.


    Azure Active Directory B2C


    У Microsoft есть сервис, который называется Azure Active Directory B2C. С помощью админов удалось настроить синхронизацию нашей AD с инстансом Azure AD и настроить вход через наш Active Directory Federation Services (ADFS).


    Настройка политик входа в Azure B2C


    На момент написания статьи сервис находится в превью версии, поэтому через UI можно настроить только самые примитивные сценарии, вроде входа через Google+ или Facebook. Вход через Active Directory производится через загрузку xml-файлов через Identity Experience Framework. На отладку сценариев входа ушло около восьми часов + ещё день на рефакторинг входа мобилки и прикручивание провайдера аутентификации от Microsoft.


    На бекенде потребовалось только указать новый IdentityProvider и Audience.


    Для того, чтобы настроить вход, потребуется скачать репозиторий и пройти процедуру настройки, описанную в статье. Всего несколько часов вы программируете на xml — и вуаля! Ваш клиент аутентифицируется через серверы Azure.


    Узреть и ужаснуться
    UsernamestringTextBoxUser's Object's Tenant IDstringTenant identifier (ID) of the user object in Azure AD.User's Object IDstringObject identifier (ID) of the user object in Azure AD.Sign in namestringTextBoxEmail AddressstringEmail address to use for signing in.TextBoxPasswordstringEnter passwordPasswordNew PasswordstringEnter new passwordPasswordConfirm New PasswordstringConfirm new passwordPasswordPassword PoliciesstringPassword policies used by Azure AD to determine password strength, expiry etc.client_idstringSpecial parameter passed to EvoSTS.Special parameter passed to EvoSTS.resource_idstringSpecial parameter passed to EvoSTS.Special parameter passed to EvoSTS.SubjectstringAlternativeSecurityIdstringMailNickNamestringYour mail nick name as stored in the Azure Active Directory.Identity ProviderstringDisplay NamestringYour display name.TextBoxPhone NumberstringXXX-XXX-Your telephone numberVerified Phone NumberstringXXX-XXX-Your office phone number that has been verifiedNew Phone Number EnteredbooleanUserId for MFAstringEmail AddressstringEmail address that can be used to contact you.TextBoxAlternate Email AddressesstringCollectionEmail addresses that can be used to contact the user.UserPrincipalNamestringYour user name as stored in the Azure Active Directory.UPN User NamestringThe user name for creating user principal name.User is newbooleanExecuted-SelfAsserted-InputstringA claim that specifies whether attributes were collected from the user.AuthenticationSourcestringSpecifies whether the user was authenticated at Social IDP or local account.ncastringSpecial parameter passed for local account authentication to login.microsoftonline.com.grant_typestringSpecial parameter passed for local account authentication to login.microsoftonline.com.scopestringSpecial parameter passed for local account authentication to login.microsoftonline.com.objectIdFromSessionbooleanParameter provided by the default session management provider to indicate that the object id has been retrieved from an SSO session.isActiveMFASessionbooleanParameter provided by the MFA session management to indicate that the user has an active MFA session.Given NamestringYour given name (also known as first name).TextBoxSurnamestringYour surname (also known as family name or last name).TextBoxLineMarkers, MetaRefresh~/tenant/default/exception.cshtml~/common/default_page_error.htmlurn:com:microsoft:aad:b2c:elements:globalexception:1.1.0Error page~/tenant/default/idpSelector.cshtml~/common/default_page_error.htmlurn:com:microsoft:aad:b2c:elements:idpselection:1.0.0Idp selection pageSign in~/tenant/default/idpSelector.cshtml~/common/default_page_error.htmlurn:com:microsoft:aad:b2c:elements:idpselection:1.0.0Idp selection pageSign up~/tenant/default/unified.cshtml~/common/default_page_error.htmlurn:com:microsoft:aad:b2c:elements:unifiedssp:1.0.0Signin and Signup~/tenant/default/multifactor-1.0.0.cshtml~/common/default_page_error.htmlurn:com:microsoft:aad:b2c:elements:multifactor:1.1.0Multi-factor authentication page~/tenant/default/selfAsserted.cshtml~/common/default_page_error.htmlurn:com:microsoft:aad:b2c:elements:selfasserted:1.1.0Collect information from user page~/tenant/default/updateProfile.cshtml~/common/default_page_error.htmlurn:com:microsoft:aad:b2c:elements:selfasserted:1.1.0Collect information from user page~/tenant/default/selfAsserted.cshtml~/common/default_page_error.htmlurn:com:microsoft:aad:b2c:elements:selfasserted:1.1.0Local account sign up page~/tenant/default/selfAsserted.cshtml~/common/default_page_error.htmlurn:com:microsoft:aad:b2c:elements:selfasserted:1.1.0Local account change password pagefacebook.comFacebookFacebookfacebookhttps://www.facebook.com/dialog/oauthhttps://graph.facebook.com/oauth/access_tokenGET0jsonLocal Account SignInLocal Account SignInWe can't seem to find your accountYour password is incorrectLooks like you used an old passwordhttps://sts.windows.net/https://login.microsoftonline.com/{tenant}/.well-known/openid-configurationhttps://login.microsoftonline.com/{tenant}/oauth2/tokenid_tokenqueryemail openidfalsePOSTPhoneFactorPhoneFactorapi.phonefactortrueAzure Active DirectoryAzure Active DirectoryfalseWritetrueYou are already registered, please press the back button and sign in instead.falseReadtrueUser does not exist. Please sign up before you can sign in.falseWritetruefalseReadtrueAn account could not be found for the provided user ID.falseWritetruefalseWritefalsetruefalseReadtruefalseWritefalsetruefalseSelf AssertedUser ID signupapi.selfassertedUser ID signupapi.selfasserted.profileupdatefalseLocal AccountEmail signupIpAddressapi.localaccountsignupCreateLocal Account SigninSignUpWithLogonEmailExchangeEmailapi.selfassertedfalseReset password using email addressIpAddressapi.localaccountpasswordresetfalseChange password (username)api.localaccountpasswordresetSession ManagementNoop Session Management ProviderSession Mananagement ProviderSession Mananagement ProvidertrueSession Mananagement ProviderTrustframework Policy Engine TechnicalProfilesTrustframework Policy Engine Default Technical Profile{service:te}Token IssuerJWT IssuerJWT{service:te}objectIdtrueobjectIdSkipThisOrchestrationStepauthenticationSourcelocalAccountAuthenticationSkipThisOrchestrationStepobjectIdSkipThisOrchestrationStepauthenticationSourcesocialIdpAuthenticationSkipThisOrchestrationStepobjectIdSkipThisOrchestrationStepisActiveMFASessionSkipThisOrchestrationStepnewPhoneNumberEnteredSkipThisOrchestrationStepauthenticationSourcelocalAccountAuthenticationSkipThisOrchestrationStepauthenticationSourcesocialIdpAuthenticationSkipThisOrchestrationStep

    Тут я привёл базовую политику входа. Для того, чтобы всё работало, требуется загрузить ещё три файла (они гораздо меньше, но вам всё равно придётся залезть в базовую политику).


    Аутентификация Azure Active Directory B2C через ADFS выглядит следующим образом:



    Код аутентификации для Active Directory
    public class AuthImpl {
        private static final String TAG = AuthImpl.class.getSimpleName();
        private Activity activity;
        private PublicClientApplication authClient;
        private AuthTokenProvider authTokenProvider;
        private String[] scopes;
        @Inject
        AuthImpl(AppCompatActivity activity, PublicClientApplication authClient, AuthTokenProvider authTokenProvider)
        {
            this.activity = activity;
            this.authClient = authClient;
            this.authTokenProvider = authTokenProvider;
            this.scopes = Constants.SCOPES.split("\\s+");
        }
        public boolean isUserAuthenticated() {
            return AndroidHelpers.isEmulator() || getCurrentUser() != null;
        }
        private User getCurrentUser() {
            try {
                return Helpers.getUserByPolicy(authClient.getUsers(), Constants.SISU_POLICY);
            } catch (MsalClientException e) {
                /* No token in cache, proceed with normal unauthenticated app experience */
                Log.d(TAG, "MSAL Exception Generated while getting users: " + e.toString());
            } catch (IndexOutOfBoundsException e) {
                Log.d(TAG, "User at this position does not exist: " + e.toString());
            }
            return  null;
        }
        public Single login()
        {
            /* Attempt to get a user and acquireTokenSilently
             * If this fails we will do an interactive request
             */
            return Single.create(emitter -> {
                try {
                    User currentUser = Helpers.getUserByPolicy(authClient.getUsers(), Constants.SISU_POLICY);
                    if (currentUser != null) {
                /* We have 1 user */
                        authClient.acquireTokenSilentAsync(
                                scopes,
                                currentUser,
                                String.format(Constants.AUTHORITY, Constants.TENANT, Constants.SISU_POLICY),
                                false,
                                getAuthSilentCallback(emitter));
                    } else {
                /* We have no user */
                        authClient.acquireToken(activity, scopes, getAuthInteractiveCallback(emitter));
                    }
                } catch (MsalClientException e) {
            /* No token in cache, proceed with normal unauthenticated app experience */
                    Log.d(TAG, "MSAL Exception Generated while getting users: " + e.toString());
                } catch (IndexOutOfBoundsException e) {
                    Log.d(TAG, "User at this position does not exist: " + e.toString());
                }
            });
        }
        //
        // App callbacks for MSAL
        // ======================
        // getActivity() - returns activity so we can acquireToken within a callback
        // getAuthSilentCallback() - callback defined to handle acquireTokenSilent() case
        // getAuthInteractiveCallback() - callback defined to handle acquireToken() case
        //
        /* Callback used in for silent acquireToken calls.
         * Looks if tokens are in the cache (refreshes if necessary and if we don't forceRefresh)
         * else errors that we need to do an interactive request.
         */
        private AuthenticationCallback getAuthSilentCallback(final SingleEmitter emitter) {
            return new AuthenticationCallback() {
                @Override
                public void onSuccess(AuthenticationResult authenticationResult) {
                    /* Successfully got a token, call api now */
                    Log.d(TAG, "Successfully authenticated");
                    emitter.onSuccess(new AuthResultDto(authenticationResult.getIdToken()));
                }
                @Override
                public void onError(MsalException exception) {
                    /* Failed to acquireToken */
                    Log.d(TAG, "Authentication failed: " + exception.toString());
                    if (exception instanceof MsalClientException) {
                        /* Exception inside MSAL, more info inside MsalError.java */
                        emitter.onError(exception);
                        assert true;
                    } else if (exception instanceof MsalServiceException) {
                        /* Exception when communicating with the STS, likely config issue */
                        emitter.onError(exception);
                        assert true;
                    } else if (exception instanceof MsalUiRequiredException) {
                        /* Tokens expired or no session, retry with interactive */
                        authClient.acquireToken(activity, scopes, getAuthInteractiveCallback(emitter));
                    }
                }
                @Override
                public void onCancel() {
                    /* User canceled the authentication */
                    Log.d(TAG, "User cancelled login.");
                    emitter.onSuccess(new AuthResultDto(true));
                }
            };
        }
        /* Callback used for interactive request.  If succeeds we use the access
         * token to call the api. Does not check cache.
         */
        private AuthenticationCallback getAuthInteractiveCallback(final SingleEmitter emitter) {
            return new AuthenticationCallback() {
                @Override
                public void onSuccess(AuthenticationResult authenticationResult) {
                    /* Successfully got a token, call api now */
                    Log.d(TAG, "Successfully authenticated");
                    Log.d(TAG, "ID Token: " + authenticationResult.getIdToken());
                    authTokenProvider.setAuthToken(authenticationResult.getIdToken());
                    emitter.onSuccess(new AuthResultDto(authenticationResult.getIdToken()));
                }
                @Override
                public void onError(MsalException exception) {
                    /* Failed to acquireToken */
                    Log.d(TAG, "Authentication failed: " + exception.toString());
                    if (exception instanceof MsalClientException) {
                        /* Exception inside MSAL, more info inside MsalError.java */
                        emitter.onError(exception);
                        assert true;
                    } else if (exception instanceof MsalServiceException) {
                        /* Exception when communicating with the STS, likely config issue */
                        emitter.onError(exception);
                        assert true;
                    }
                }
                @Override
                public void onCancel() {
                    /* User canceled the authentication */
                    Log.d(TAG, "User cancelled login.");
                    emitter.onSuccess(new AuthResultDto(true));
                }
            };
        }
        static final int BROWSER_FLOW = 1001;
        boolean handleInteractiveRequestRedirect(int requestCode, int resultCode, final Intent data){
            if(requestCode == BROWSER_FLOW)
            {
                authClient.handleInteractiveRequestRedirect(requestCode, resultCode, data);
                return true;
            }
            return false;
        }
    }

    Всё было отлично. Мы запустили тестовую эксплуатацию приложения на наших пользователей, но столкнулись с несколькими блокирующими проблемами:


    1. Компонента аутентификации полностью не работал, если на устройстве не был установлен Google Chrome.
    2. Авторизация в Azure AD для новых пользователей перестала работать.
    3. После удачного логина через ADFS было невозможно разлогиниться. Для разлогина требовалось очищать кэш браузера Chrome на устройстве.

    Переезд на Google Firebase Auth


    Вопрос с синхронизацией пользователей долго оставался подвешенным и, так как не было понятно, сколько времени потребует решение этой проблемы, мы экстренно переехали на другого провайдера. В этот раз мы попробовали Google Firebase Auth, так как он обладал нативным интерфейсом для входа и хорошо работал на всех наших тестовых устройствах.


    Так как на клиенте мы абстрагировались от конкретного провайдера, то переехали на Google Firebase Auth за час.


    Из доработок на бекенде потребовалось добавить код для скачивания сертификатов Google, которыми Firebase подписывает токены авторизации.


    Код аутентификации Firebase
     private final FirebaseAuth authInstance;
      private CompositeDisposable disposables;
      private EventSubscriber eventSubscriber;
      public Single login(boolean forceRefreshToken)
        {
            if (disposables != null) {
                disposables.dispose();
            }
            disposables = new CompositeDisposable();
            return Single.create(emitter -> {
                FirebaseUser user = authInstance.getCurrentUser();
                if (user == null || !user.isEmailVerified()) {
                    List providers = Arrays.asList(
                            new AuthUI.IdpConfig.EmailBuilder().setRequireName(true).build(),
                            new AuthUI.IdpConfig.GoogleBuilder().build());
                    activity.startActivityForResult(
                            AuthUI.getInstance()
                                    .createSignInIntentBuilder()
                                    .setAvailableProviders(providers)
                                    .build(),
                            MainActivity.REQUES_LOGIN);
                } else {
                    if (forceRefreshToken) {
                        refreshToken();
                    } else {
                        emitter.onSuccess(new AuthResultDto(false));
                    }
                }
                disposables.add(this.eventSubscriber.getEvent(new AuthResultDto(true))
                        .subscribe(e ->  {
                            emitter.onSuccess(e);
                            disposables.dispose();
                        }));
            });
        }
        private void refreshToken() {
            FirebaseUser user = authInstance.getCurrentUser();
            user.getIdToken(true)
                    .addOnCompleteListener(task -> {
                        if (task.isSuccessful()) {
                            GetTokenResult tokenResult = task.getResult();
                            String idToken = tokenResult.getToken();
                            // Сохраняем idToken
                            eventSubscriber.Publish(new AuthResultDto( false));
                            // ...
                        } else {
                            AuthResultDto result = new AuthResultDto( false);
                            result.Error = new Exception(task.getException());
                            eventSubscriber.Publish(new AuthResultDto(false));
                        }
            });
        }
    

    Сейчас, когда мы запустились в продакшн на Firebase, проблему с синхронизацией данных в Azure AD удалось решить. Поэтому в перспективе мы возможно вернёмся на Azure.


    Обо всём ещё разок


    Мы попробовали поработать с тремя разными провайдерами аутентификации в реальном проекте и, исходя из нашего опыта, я могу порекомендовать следующее.


    Auth0 — отличный провайдер, очень дружественно настроенный к разработчикам. Приятное API, нет проблем с интеграцией. Если у вас нет каких-то административных барьеров, то рекомендую к использованию :)


    Azure Active Directory B2C — хорош для Enterprise. Скорее всего, удастся договориться с админами и юристами. Пока ещё довольно сырой, поэтому приходится писать конфиги в xml. Ещё одна особенность платформы — для администрирования инстанса B2C в России требуется учётка того, кто привязал карту к аккаунту. Это неудобно, пока идёт отладка и тестирование.


    Firebase Auth is the lightest option. Suitable if you do not need complex login scripts and just have authentication. Of the minuses - a rather ascetic admin panel and a small list of default authentication sources.


    What else can I add?


    Using standards, in particular OpenId Connect, allowed us to:


    - quickly and cheaply adapt our solution on the client,
    - save resources on the development of the server side,
    - do not waste time debugging and debugging custom authentication code.


    All secure backend and easy authentication at the front :)


    Also popular now: