Внедряем Sign in with Apple — систему авторизации от Apple

    Привет, Хабр!

    Этим летом на конференции WWDC 2019 Apple представила собственную систему авторизации Sign in with Apple  и сделала ее обязательной для всех приложений в App Store, которые используют вход через соцсети. Исключение составляют образовательные, корпоративные, правительственные и бизнес-приложения, использующие собственную авторизацию. К Sign in with Apple Apple сделала качественную документацию, и в этой статье мы на примере ЦИАН расскажем, как внедрить ее в свой сервис.



    Настраиваем Apple Developer Account


    Работа по интеграции начинается с настройки аккаунта разработчика. Сначала нужно включить опцию Sign In with Apple для вашего App ID. Для этого заходим в список идентификаторов в Apple Developer Account, выбираем необходимый App ID и включаем для него опцию Sign In with Apple.

    Теперь настраиваем Service ID — уникальный идентификатор web-приложения, который понадобится для обращения к Sign in with Apple API. Всего на один App ID можно создать до 5 Service ID. Для этого нажимаем кнопку создания идентификаторов, выбираем Service ID, заполняем необходимые поля и нажимаем Edit в поле Sign In With Apple. Откроется форма, где выбираем правильный Primary App ID, указываем веб-домен и перечислям URL для редиректа после успешного логина. Надо учитывать, что можно ввести только 10 Return URLs:



    Для сохранения нажимаем Save, Continue и Register. Да, при любых изменениях конфигурации необходимо нажимать все три кнопки, иначе изменения не вступят в силу.

    Теперь в списке Service ID выбираем созданный идентификатор и опять нажимаем Edit в поле Sign In With Apple. В открывшемся окне у поля с веб-адресом видим две новые кнопки:



    Этот файл необходим, чтобы Apple верифицировала ваш ресурс. Скачиваем его и размещаем его на своем ресурсе. Сразу у нас этот финт не сработал: когда наши админы добавили файл, то по указанному url срабатывал редирект (302) на файл, лежащий в другом месте, и Apple его не верифицировал. Тогда пришлось размещать файл по прямому доступу по URL (200). После того как Apple успешно проверит файл, рядом с доменом загорится зеленая галочка:



    Из раздела идентификаторов переходим в раздел Keys и создаем новый ключ. Для этого ставим галочку Sign In with Apple и нажимаем сначала Configure, чтобы проверить App ID, затем Continue:



    На следующем экране обязательно скачиваем файл с ключом и сохраняем его в безопасном месте, так как после ухода с этого экрана ключ будет недоступен для скачивания. На этой же странице можно увидеть Key ID, который нам еще понадобится:



    Для пользователей у Sign In with Apple есть бонус: она позволяет предоставить фейковый e-mail, на который можно писать только с доверенных адресов. В этом случае нужна дополнительная настройка. Открываем раздел More, нажимаем Configure в разделе Sign In with Apple и вписываем свой URL:



    Добавляем кнопку Sign In with Apple в iOS-приложение


    ЦИАН работает на трех платформах:  iOS, Android, Web. Для iOS есть нативное SDK, поэтому авторизация будет выглядеть следующим образом:



    Чтобы добавить в iOS-приложение Sign in with Apple, добавляем кнопку ASAuthorizationAppleIDButton и вешаем на нее обработчик нажатия:

    let appleIDProvider = ASAuthorizationAppleIDProvider()
    let request = appleIDProvider.createRequest()
    request.requestedScopes = [.fullName, .email]
    
    let authorizationController = ASAuthorizationController(authorizationRequests: [request])
    authorizationController.delegate = self
    authorizationController.presentationContextProvider = self
    authorizationController.performRequests()
    

    Кроме ASAuthorizationAppleIDProvider, обратите внимание еще на ASAuthorizationPasswordProvider, который позволяет получать связки «логин-пароль» из Keychain. 

    Теперь мы реализуем ASAuthorizationControllerPresentationContextProviding:

    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return self.view.window!
    }
    

    Создаем делегат ASAuthorizationControllerDelegate, который сообщает об успехе или ошибке:

    public func authorizationController(
      controller: ASAuthorizationController, 
      didCompleteWithAuthorization authorization: ASAuthorization
    ) {
    guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential,
    let code = appleIDCredential.authorizationCode,
    let codeStr = String(data: code, encoding: .utf8) else {
            // что-то явно пошло не так
            // обработать как ошибку
            return
    }
    
    let email = appleIDCredential.email
    let firstName = appleIDCredential.fullName?.givenName
    let lastName = appleIDCredential.fullName?.familyName
    	// создать или залогиниться в своей системе с помощью кода авторизации codeStr
    }
    
    public func authorizationController(
      controller: ASAuthorizationController,
      didCompleteWithError error: Error
    ) {
      // обработка ошибки
    }
    

    Полученный authorizationCode мы отправляем на сервер и ждем ответа от бэкенда об успешности авторизации в нашей системе.

    Реализуем Sign in with Apple для web и Android


    Внезапно, для Android и Web Apple не предоставляет SDK, поэтому в обоих случаях  нужно открыть страницу авторизации от Apple и процесс будет иным:



    URL для страницы авторизации выглядит следующим образом:

    https://appleid.apple.com/auth/authorize?\
    state=abvgd&\
    response_type=code&\
    client_id=ServiceID&\
    scope=email+name&\
    response_mode=form_post&\ redirect_uri=https%3A%2F%2Fcian.ru%2Fauth%2Fsome-callback%2F%3Ftype%3Dappleid
    

    Рассмотрим его параметры:

    • client_id — Service ID, который регистрировали выше.
    • redirect_uri — URI, куда пользователь перенаправляется после успешной аутентификации через AppleID. Этот URI мы указывали выше при настройке Apple Developer.
    • state — идентификатор сессии пользователя, который Apple вернет при вызове redirect_uri, чтобы мы могли проверить отправителя. Правило генерации этого параметра можете придумать самостоятельно, например, рандомную строку.
    • scope — в этом параметре указывается, какая нужна информация от пользователя. Например, name, email или сразу оба, как в примере выше.
    • response_type — этот параметр указывает, в каком виде нужен ответ. Он может быть code или id_token. Если выбрать id_token, то его нужно уточнить параметром response_mode, в котором можно указать query, fragment и form_post.

    После успешной двухфакторной аутентификации через appleID Apple вызовет указанный redirect_uri и передаст параметры state и code:

    curl -X POST \
    'https://www.cian.ru/some-callback/?type=appleid' \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    --data ' \
    state=abvgd&\
    code=12345&\
    user={"name":{"firstName":"Tanya","lastName":"Sviridova"},"email":"someemail@gmail.com"}'
    

    В параметре code передается одноразовый код аутентификации пользователя, который действует в течение 5 минут. В параметре state — идентификатор сессии, отправленный при создании формы авторизации, а в параметре user — данные пользователя.

    Получение данных  


    На всех клиентах, чтобы сохранить данные пользователя, нужно получить от Apple access_token. Для этого сначала запрашиваем authorization_code:

    curl -X POST https://appleid.apple.com/auth/token -d '\
    client_id=some_client_id&\
    code=12345&\
    client_secret=jwt_part1.jwt_part2.jwt_part3&\
    grant_type=authorization_code'
    

    В этом запросе: 

    • в client_id указывается созданный для web-приложений ServiceID и AppID для iOS-приложения.
    • code — мы получили выше после редиректа или передали с iOS-клиента
    • в параметре grant_type передаем цель получения токена: авторизация (authorization_code) или продление токена (refresh_token)
    • в параметре client_secret — JSON Web Tokens на основе секретного ключа, полученного при регистрации приложения.

    Создать JSON Web Tokens можно на Python:

    claims = {
    	'iss': APPLEID_TEAM_ID,
    	'aud': 'https://appleid.apple.com',
    	'sub': client_id,
    	'iat': current_timestamp,
    	'exp': current_timestamp + expire_period,
    }
    
    headers = {'kid': 'APPLEID_KEY_ID', 'alg': 'ES256'}
    
    client_secret = jwt.encode(payload=claims, key=secret_key, algorithm='ES256', headers=headers).decode('utf-8')
    

    Если все прошло успешно, то в ответе придут такие параметры:
    {
      "access_token":"ufhzch",
      "token_type":"Bearer",
      "expires_in":3600,
      "refresh_token":"some_refresh_token",
      "id_token":"some_long_signed_jwt_token"
    }
    

    Ура, вот и access_token. Вместе с ним приходит refresh_token, которым можно обновить при необходимости access_token. 

    Информация о пользователе хранится в поле id_token, но его нужно декодировать:

    public_key = jwt.algorithms.RSAAlgorithm.from_jwk(
        json.dumps(apple_public_key)
    )
    data = jwt.decode(
        id_token,
        public_key,
        algorithm="RS256",
        verify=True,
        audience=client_id,
    )
    

    Apple_public_key — это публичный ключ, который можно получить по ссылке. 

    После декодирования получаем:

    data = {
      "iss": "https://appleid.apple.com",
      "aud": client_id,
      "exp": 1570379521,
      "iat": 1570378921,
      "sub": "уникальный идентификатор пользователя",
      "at_hash": "8ZDF6j786IQf9mA",
      "email": "someemail@gmail.com",
      "email_verified": "true",
      "auth_time": 1570378804
    }
    

    Email передается только один раз, когда пользователь впервые авторизуется в вашем сервисе через Sign in with Apple. В следующий раз Apple передаст эти данные только в том случае, если пользователь самостоятельно отвяжет ваше приложение. Этим авторизация от Apple отличается от других сервисов, где данные можно получить через API, и мы не нашли информацию о том, что они планируют реализовать что-то подобное.

    В этом ответе нам нужны параметры sub, который передается каждый раз, и email, поэтому мы сохраняем их у себя в системе и сообщаем клиенту о успешной авторизации. PROFIT.

    Первые результаты


    После выхода новой версии ЦИАН с Sign in with Apple на неё в первый же день день пришлась треть новых регистраций на iOS 13, и сейчас для всех версий iOS она занимает второе место, уступая только VK. На сайте регистраций с помощью AppleID немного, но их число потихоньку растет. А сейчас у нас в планах включить авторизацию через АppleID на Android-приложениях, и посмотреть, сколько пользователей будет регистрироваться таким хитрым образом.  
    Циан
    Компания

    Комментарии 10

      +3
      Слава богу, что Apple это реализовала, а то кнопка «Войти через FaceBook» уже дико достала.
        0

        «войти через фейсбук» это ладно, я удивился когда увидел «войти через LINE».

          0

          А в LINE можно войти через facebook

          +1
          А в чём проблема, собственно? :)
            0

            Тем что я не готов регистрироваться в фейсбук лишь для того чтобы на сайтах сторонних регистрироваться. Фейсбук по-моему уже давно у всех с помойкой ассоциируется и куча народу оттуда уже удалилась

          0
          Уже отказывают в публикации если не внедрить?
            0
            ЦИАНу не отказывали по этой причине.
            И лично я еще не слышал о таких случаях)
              0
              Мне отказали. Вот, сижу, изучаю…

              Да и Циану вход из Apple особенных плюсов не принёс, ибо на текущий момент они его убрали под "...":
              Вход в циан на момент написания коммента
              image
              0
              А можно то же самое, только с поддержкой iOS 12?
              ASAuthorizationController появился только в iOS 13
                0
                Тоже самое не сделаешь, так как вся суть в SDK iOS 13, в котором и содержится код с нативной реализацией.
                Но, если необходимо поддержать iOS 12, можно будет сделать по аналогии с Андроидом, т.е. авторизоваться через webView. Это, конечно, не так симпатично, но задачу решит.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое