Недавно мне потребовалось разобраться, как делается аутентификация на OpenId Connect на ASP.NET Core. Начал с примеров, быстро стало понятно, что чтения спецификации не избежать, затем пришлось уже перейти к чтению исходников и статей разработчиков. В результате возникло желание собрать в одном месте всё, что необходимо для того, чтобы понять, как сделать рабочую реализацию OpenId Connect Implicit Flow на платформе ASP.NET Core, при этом понимая, что Вы делаете.  Статья про специфику имплементации, поэтому рекомендую воспроизводить решение по предложенному в статье коду, иначе будет трудно уловить контекст. Большинство значимых замечаний в комментариях и в тексте статьи содержат ссылки на источники. Некоторые термины не имеют общепринятых переводов на русский язык, я оставил их на английском.  Если Вы понимаете OpenId Connect, можете начинать читать со следующей части.  OpenId Connect (не путать с OpenId) — протокол аутентификации, построенный на базе протокола авторизации OAuth2.0. Дело в том, что задачу OAuth2 входят вопросы только авторизации пользователей, но не их аутентификации. OpenID Connect также задаёт стандартный способ получения и представления профилей пользователей в качестве набора значений, называемых claims. OpenId Connect описывает UserInfo endpoint, который возвращает эти информацию. Также он позволяет клиентским приложениям получать информацию о пользователе в форме подписанного JSON Web Token (JWT), что позволяет слать меньше запросов на сервер. Начать знакомство с протоколом имеет смысл с официального сайта, затем полезно почитать сайты коммерческих поставщиков облачных решений по аутентификации вроде Connect2id, Auth0 и Stormpath. Описание всех нужных терминов не привожу, во-первых это была бы стена текста, а во вторых всё необходимое есть по этим ссылкам.  Если Identity Server Вам не знаком, рекомендую начать с чтения его прекрасной документации, а также отличных примеров вроде этого.  Мы реализуем OpenId Connect Implicit Flow, который рекомендован для JavaScript-приложений, в браузере, в том числе для SPA. В процессе мы чуть глубже, чем это обычно делается в пошаговых руководствах, обсудим разные значимые настройки. Затем мы посмотрим, как работает наша реализация с точки зрения протокола OpenId Connect, а также изучим, как имплементация соотносится с протоколом.  Основные авторы обеих библиотек — Брок Аллен и Доминик Брайер.  У нас будет 3 проекта:  Сценарий взаимодействия таков: клиентское приложение Client авторизуется при помощи сервера аутентификации IdentityServer и получает access_token (JWT), который затем использует в качестве Bearer-токена для вызова веб-сервиса на сервере Api. Стандарт OpenId Connect описывает разные варианты порядка прохождения аутентификации. Эти варианты на языке стандарта называются Flow.
Implicit Flow, который мы рассматриваем в этой статье, включает такие шаги:  Для того, чтобы сильно сэкономить на написании станиц, связанных с логином и логаутом, будем использовать официальный код Quickstart.  Запускать Api и IdentityServer в процессе выполнения этого упражнения рекомендую через dotnet run — IdentityServer пишет массу полезной диагностической информации в процессе своей работы, данная информация сразу будет видна в консоли.  Для простоты предполагается, что все проекты запущены на том же компьютере, на котором работает браузер пользователя.  Давайте приступим к реализации. Для определённости будем предполагать, что Вы используете Visual Studio 2017 (15.3). Готовый код решения можно посмотреть здесь
Создайте пустой solution OpenIdConnectSample. Большая часть кода основана на примерах из документации IdentityServer, однако код в данной статье дополнен тем, чего, на мой взгляд, не хватает в официальной документации, и аннотирован. Рекомендую ознакомиться со всеми официальными примерами, мы же поглубже рассмотрим именно Implicit Flow. Создайте solution с пустым проектом, в качестве платформы выберите ASP.NET Core 1.1. Установите такие NuGet-пакеты  Версии пакетов здесь значимы, т.к. Install-Package по умолчанию устанавливает последние версии. Хотя авторы уже сделали порт IdentityServer на Asp.NET Core 2.0 в dev-ветке, на момент написания статьи, они ещё не портировали Quickstart UI. Различия в коде нашего примера для .NET Core 1.1 и 2.0 невелики.  Измените метод Main Program.cs так, чтобы он выглядел следующим образом Затем в Startup.cs Настройки информации для клиентских приложений Эти настройки добавляют поддержку claim sub, это минимальное требование для соответствия нашего токена OpenId Connect, а также claim scope profile, включающего описанные стандартом OpenId Connect поля профиля типа имени, пола, даты рождения и подобных. Это аналогичные предыдущим настройки, но информация предназначается для API Сами клиентские приложения, нужно чтобы сервер знал о них Тестовые пользователи, обратите внимание, что bob у нас админ В этом методе мы указываем настройки IdentityServer, в частности сертификаты, используемые для подписывания токенов, настройки scope в смысле OpenId Connect и OAuth2.0, настройки приложений-клиентов, а также настройки пользователей.  Теперь чуть подробнее. AddIdentityServer регистрирует сервис IdentityServer в механизме разрешения зависимостей ASP.NET Core, это нужно сделать, чтобы была возможность добавить его как middleware в Configure. Метод Configure должен выглядеть так Скачайте из официального репозитория Starter UI для IdentityServer, затем скопируйте файлы в папку проекта, так чтобы папки совпали по структуре, например wwwroot с wwwroot.  Проверьте, что проект компилируется. Данный проект — игрушечный сервер API с ограниченным доступом.  Добавьте в solution ещё один пустой проект Api, в качестве платформы выберите ASP.NET Core 1.1. Т.к. мы не собираемся создавать полноценное веб-приложение в данном проекте, а лишь легковесный веб-сервис, отдающий JSON, ограничимся лишь MvcCore middleware вместо полного Mvc.  Добавьте нужные пакеты, выполнив эти команды в Package Manager Console Начнём с того, что добавим нужные настройки Kestrel в Program.cs В Startup.cs потребуется несколько меньше изменений.
Для ConfigureServices А вот так должен выглядеть Configure Осталось добавить наш контроллер, он возвращает текущие Claims пользователя, что удобно для того, чтобы понимать, как middleware аутентификации IdentityServer расшифровал access_token.
Добавьте в проект единственный контроллер IdentityController.
Cодержимое файла должно быть таким. Убедитесь, что проект компилируется. Этот проект фактически не содержит значимой серверной части. Весь серверный код — это просто настройки веб-сервер Kestrel, с тем чтобы он отдавал статические файлы клиента.  Так же, как и прошлых 2 раза добавьте в решение пустой проект, назовите его Client. Установите пакет для работы со статическими файлами.  Измените файл Program.cs Класс Startup должен содержать такой код.  Клиентский код на JavaScript, с другой стороны, и содержит всю логику аутентификации и вызовов Api.  Мы по одному добавим в папку wwwroot проекта следующие файлы.  index.html
Добавьте новый HTML-файл с таким названием в папку wwwroot проекта.  oidc-client.js
Скачайте этот файл отсюда (1.3.0) и добавьте в проект.  app.js
Добавьте новый JavaScript-файл с таким названием в папку wwwroot проекта. Добавьте  в начале файла для поддержки IntelliSense.  Вставьте этот код к началу app.js Первой строкой, пользуясь совместимостью по вызываемым методам, устанавливаем стандартную консоль браузера в качестве стандартного логгера для oidc-client. Второй строкой просим выводить все сообщения. Это позволит увидеть больше подробностей, когда мы перейдём ко второй части статьи, и будем смотреть, как же наша имплементация работает.  Теперь давайте по частям добавим остальной код в этот файл.  Эта часть кода самая длинная, и, пожалуй, самая интересная. Она содержит настройки библиотеки основного объекта UserManager библиотеки oidc-client, а также его создание. Рекомендую ознакомиться с самими настройками и комментариями к ним.  Давайте теперь добавим обработчики для кнопок и подписку на них.  Осталось добавить пару утилит В принципе, на этом можно было бы и заканчивать, но требуется добавить ещё две страницы, которые нужны для завершения процедуры входа. Добавьте страницы с таким кодом в wwwroot.  callback.html callback-silent.html Готово! Запускать проекты рекомендую так: запускаете консоль, переходите в папку проекта, выполняете команду dotnet run. Это позволит видеть что IdentityServer и другие приложения логируют в консоль.  Запустите вначале IdentityServer и Api, а затем и Client. Откройте страницу http://localhost:5003/index.html Client.
На этом этапе Вы можете захотеть очистить консоль при помощи clear().  Теперь давайте настроим консоль, чтобы на самом деле видеть всю интересную информацию.
Например, для Chrome 60 настройки консоли должны выглядеть так.  Во вкладке Network инструментов разработчика Вы можете захотеть поставить галочку напротив Preserve log чтобы редиректы не мешали в дальнейшем проверять значения различных параметров.  Обновите страницу при помощи CTRL+F5.  Посмотрим, какие действия соответствуют первым двум шагам спецификации.
1. Клиент готовит запрос на аутентификацию, содержащий нужные параметры запроса.
2. Клиент шлёт запрос на аутентификацию на сервер авторизации.
Кликните на кнопку Login.  Взаимодействие с сервером авторизации начинается с GET-запроса на адрес
http://localhost:5000/.well-known/openid-configuration
Этим запросом oidc-client получает метаданные нашего провайдера OpenId Connect (рекомендую открыть этот адрес в другой вкладке), в том числе authorization_endpoint
http://localhost:5000/connect/authorize Обратите внимание, что для хранения данных о пользователе используется WebStorage. oidc-client позволяет указать, какой именно объект будет использоваться, по умолчанию это sessionStorage.  В этот момент будет послан запрос на аутентификацию на authorization_endpoint с такими параметрами строки запроса Обратите внимание, что redirect_uri соответствует адресу, который мы указали для нашего клиента с client_id js в настройках IdentityServer. Т.к. пользователь ещё не аутентифицирован, IdentityServer вышлет в качестве ответа редирект на форму логина.  Затем браузер перенаправлен на http://localhost:5000/account/login.  3. Сервер авторизации аутентифицирует конечного пользователя.
4. Сервер авторизации получает подтверждение от конечного пользователя.
5. Сервер авторизации посылает конечного пользователя обратно на клиент с id token'ом и, если требуется, access token'ом.  Вводим bob в качестве логина и password в качестве пароля, отправляем форму.
Нас вначале вновь перенаправляют на authorization_endpoint, а оттуда на страницу подтверждения в соответствии с OpenId Connect разрешения получения relying party (в данном случае нашим js-клиентом) доступа к различным scopes.  Со всем соглашаемся, отправляем форму. Аналогично форме аутентификации, в ответ на отправку формы нас перенаправляют на authorization_endpoint, данные на authorization_endpoint передаются при помощи cookie.  Оттуда браузер перенаправлен уже на адрес, который был указан в качестве redirect_uri в изначальном запросе на аутентификацию. При использовании Implicit Flow параметры передаются после #. Это нужно для того, чтобы эти значения были доступны нашему приложению на JavaScript, но при этом не отправлялись на веб-сервер.  6. Клиент валидирует id token и получает Subject Identifier конечного пользователя.  oidc-client проверяет вначале наличие сохранённого на клиенте state, затем сверяет nonce с полученным из id_token. Если всё сходится, происходит проверка самих токенов на валидность (например, проверяется подпись и наличие sub claim в id_token). На этом этапе происходит чтение чтение содержимого id_token о объект профиля пользователя библиотеки oidc-client на стороне клиента.  Если Вы захотите расшифровать id_token (проще всего его скопировать из вкладки Network инструментов разработчика), то увидите, что payload содержит что-то подобное at_hash, который затем используется для валидации в соответствии со стандартом.  Для access_token в нашем случае payload будет выглядеть, в том числе в соответствии с настройками, чуть иначе.  Если Вы не умеете для себя объяснять все их отличия, сейчас — прекрасный момент устранить этот пробел. Начать можно отсюда, или с повторного прочтения кода настроек IdentityServer. В случае когда проверка завершается успехом, происходит чтение claims из id_token в объект профиля на стороне клиента.  Затем, но только если указана настройка loadUserInfo, происходит обращение к UserInfo Endpoint. При этом при обращении UserInfo Endpoint для получения claims профиля пользователя в заголовке Authorization в качестве Bearer-токена используется access_token, а полученные claims будут добавлены в JavaScript-объект профиля на стороне клиента.  loadUserInfo имеет смысл использовать если Вы хотите уменьшить размер access_token, если Вы хотите избежать дополнительного HTTP-запроса, может иметь смысл от этой опции отказаться. Нажмите кнопку "Call API".
Произойдёт ajax-запрос на адрес http://localhost:5001/identity.
А именно, вначале будет OPTIONS-запрос согласно требованиями CORS т.к. мы осуществляем запрос ресурса с другого домена и используем заголовки, не входящие в список "безопасных" (Authorization, например).  Затем будет отправлен, собственно, сам GET-запрос. Обратите внимание, что в заголовке запроса Authorization будет указано значение Bearer <значение access_token>.  IdentityServer middleware на стороне сервера проверит токен. Внутри кода IdentityServer middleware проверка токенов фактически осуществляется стандартным Asp.Net Core JwtBearerMiddleware.  Пользователь будет считаться авторизованным, поэтому сервер вернёт нам ответ с кодом 200.  Отправляется GET-запрос на end_session_endpoint В ответ нас перенаправляют на страницу, содержащую данные о логауте для пользователя.  На самом деле политики позволяют задавать любые условия предоставления доступа, но я остановился на примере реализации безопасности через роли. Ролевую модель же реализуем через политики и токены, потому что это во-первых просто и наглядно, а во-вторых это наиболее часто используемый способ задания разрешений.  Попробуйте зайти вначале под пользователем alice и нажать кнопку Get Superpowers!, затем зайдите под пользователем bob и проделайте то же самое.  Нажмите Logout и залогиньтесь ещё раз, на этот раз используйте данные
Username: alice
Password: password На странице подтверждения http://localhost:5000/consent нажмите No, Do Not Allow. Вы попадёте на страницу завершения логина клиентского приложения http://localhost:5003/callback.html.
По причине того, что страница подтверждения пользователем передаёт фрагмент URL #error=access_denied, выполнение signinRedirectCallback пойдёт по другому пути, и промис в результате будет иметь статус rejected. На странице callback.html будет для промиса выполнен catch-обработчик, он выведет текст ошибки в консоль.  Скопируйте закодированный id_token из одноимённого параметра URL ответа и убедитесь, что теперь в него не входят claims, которые входят в стандартный scope profile.  Claims, которые входят в стандартный scope profile можно посмотреть тут.  При этом вызвать API получится.  В токене теперь нет claim api1 При попытке вызвать Api нам теперь возвращают 401 (Unathorized). Дождитесь устаревания access_token, нажмите кнопку Call API.  API будет вызван! Это вызвано тем, что IdentityServer использует middleware Asp.Net Core, который использует понятие ClockSkew. Это нужно для того, чтобы всё в целом работало в случае если часы на клиенте и разных серверах несколько неточны, например, не возникали ситуации вроде токена, который был выпущен на период целиком в будущем. Значение ClockSkew по умолчанию 5 минут.  Теперь подождите 5 минут и убедитесь, что вызов API теперь возвращает 401 (Unathorized). Замечание В клиентском приложении может быть полезно явно обрабатывать ответы с кодом 401, например пытаться обновить access_token.  Давайте теперь добавим в app.js в объект config код, так чтобы получилось  При помощи консоли браузера убедитесь что теперь происходит автоматическое обновление access_token. Нажмите кнопку Call API чтобы убедиться, что всё работает.  Если access_token предназначается для ресурса API и ресурс обязан проверить его валидность, в том числе не устарел ли токен, при обращении к нему, то id_token предназначен именно для самого клиентского приложения. Поэтому и проверка должна проводиться на клиенте js-клиенте. Хорошо описано тут. Если Вы следовали инструкциям, на данный момент Вы: