20

Sign in with Apple, Part 4: Web and Other Platforms

 4 years ago
source link: https://sarunw.com/posts/sign-in-with-apple-4/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

The fourth part of a series Sign in with Apple (And maybe the last part). This part is less related to us, iOS developer, let's explore it to see what we might need to do to support this on web and other platforms.

  1. Sign in with Apple, Part 1: Apps
  2. Sign in with Apple, Part 2: Private Email Relay Service
  3. Sign in with Apple, Part 3: Backend – Token verification
  4. Sign in with Apple, Part 4: Web and Other Platforms

As I remembered, Apple didn't mention OAuth or OpenID Connect in their WWDC session or documentation. Luckily Apple didn't introduce their own wheel but adopt the existing open standards OAuth 2.0 and OpenID Connect ( Hybrid Flow ). They use the same terminology and API calls. If you're familiar with these technologies, Sign in with Apple shouldn't be a problem for you. If you aren't familiar with OAuth and OpenID Connect, don't worry. I will guide you through all of the flow and dance needed.

Let's begin from the frontend, the button.

Configuring Your Webpage for Sign In with Apple

Ensure your webpage is ready to authorize users through Sign In with Apple.

Embed Sign In with Apple JS in Your Webpage

Use the script tag and link to Apple’s hosted version of the Sign In with Apple JS framework:

<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
<html>
    <head>
        <meta name="appleid-signin-client-id" content="[CLIENT_ID]">
        <meta name="appleid-signin-scope" content="[SCOPES]">
        <meta name="appleid-signin-redirect-uri" content="[REDIRECT_URI]">
        <meta name="appleid-signin-state" content="[STATE]">
    </head>
    <body>
        <div id="appleid-signin" class="signin-button" data-color="black" data-border="true" data-type="sign in"></div>
        <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
    </body>
</html>

You can also configure the authorization object using the JavaScript APIs and display a Sign In with Apple button, as in the following example:

<html>
    <head>
    </head>
    <body>
        <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
        <div id="appleid-signin" data-color="black" data-border="true" data-type="sign in"></div>
        <script type="text/javascript">
            AppleID.auth.init({
                clientId : '[CLIENT_ID]',
                scope : '[SCOPES]',
                redirectURI: '[REDIRECT_URI]',
                state : '[STATE]'
            });
        </script>
    </body>
</html>

Your page would look something like this:

VjeIjyR.png!web

At this moment, there is a bug in the script. You can monkey patch it with some CSS trick, here is mine:

.signin-button > div > div > svg {  
  height: 50px;  
  width: 100%;  
} 

Now we have a proper sign-in button.

YvErUrB.png!web

If you click on the button right now, you will get an invalid client error, which is not a surprise since our value in meta tag still be placeholder value.

iMVJJvN.png!web

To make this work, we need to provide four values. I will show you how to get those values (client id, scopes, redirect URL, and state).

Client ID & Redirect URL

First, you need to have a client id to represent your application. Apple called this Services ID in the Apple developer portal.

  1. Go to Identifiers menu in Certificates, Identifiers & Profiles
  2. Choose Services IDs

aEvqyyR.png!web

client_id

ZZ3E7v6.png!web

  1. Click the Configure button next to Sign in with Apple . This is where you define the domain your app is running and redirect URLs for OAuth flow.

quqiYfj.png!web

We just created a Services ID ( client_id ) and a redirect URL that are required to initiate Sign in with Apple (Authentication Requeststep in OpenID Connect flow).

The scope is the amount of user information requested from Apple. Right now, there are only two options name and email . You can request the user’s name or email. You can also choose to request both or neither.

State is a parameter defined in OAuth protocolused to mitigate CSRF attacks. It can be any string; just make sure it is unique and non-guessable value.

After you know how to get and generate those parameters, replace all the placeholders, and try signing in again. This time everything would work as it should be. You will be redirected to Apple.com and prompt with Sign in form.

YVfQFnN.png!web

Configuring Sign In with Apple Buttons

Use CSS styles and data attributes to display and configure Sign In with Apple buttons in browsers.

Control the size of the button by adding a class that contains the desired CSS width and height styles.

.signin-button {
  width: 210px;
  height: 40px;
}

The above example is coming from the Apple documentation, but it does not work as expected at the moment. I hack around with the following instead.

.signin-button > div > div > svg {  
  height: 50px;  
  width: 100%;  
} 

You can change the text in the button by setting the data-type property to one of the following values:

sign in
continue

Specify the background color of the Sign In with Apple button by setting the data-color property to one of the following values:

black
white

Specify the border for the Sign In with Apple button by setting the data-border property to one of the following values:

false
true

Use CSS to control the corner radius like you normally do.

.signin-button {
  border-radius: 10px;
}

We are not finished yet, but let see how far we can go with the information we have now.

  1. Click Sign in with Apple button now direct you to Apple.com and prompt users to sign in.

YVfQFnN.png!web

If this is your first time, you might see two-factor authentication dialog.

eY7RvaA.png!web

  1. After pass two-factor authentication, you will see a prompt confirming that you want to sign in to this application using your Apple ID along with information that will be shared with the app.

zuiMJvv.png!web

  1. You can edit that information by click on Edit buttons

yayEjuA.png!web

  1. Click Continue and you will be redirected back to your app, which will be failed since we didn't implement any logic to handle the redirect.

AB7jMvY.png!web

As you can see, we can go a bit far with the information we got. We redirected back to what we put in <meta name="appleid-signin-redirect-uri" content="[REDIRECT_URI]"> ( https://siwa-example.herokuapp.com/redirect ).

Handle the Redirect

This is where we handle Authentication Response . The default response_type for Authentication Request generated from Apple JS is code id_token , so we would get state , id_token , and code along with user in the response.

The Apple default button is making a request with response_mode equal to form_post (which is required if you requested any scopes) which will make an HTTP POST to the client with Authorization Response parameters.

Here is a very simple Rails application to handle the POST response.

Payload:

{
   "state": "xxx",
   "code": "yyy",
   "id_token": "zzz",
   "user": {
        "name": {
            "firstName":"John",
            "lastName":"Doe"
        },
        "email":"[email protected]"
    }
}

Application code:

# home_controller.rb
class HomeController < ApplicationController
  skip_before_action :verify_authenticity_token, only: :redirect
  
  def index
    state = Digest::SHA1.hexdigest(SecureRandom.random_bytes(1024))
    session[:state] = state
  end

  def redirect
    @state = params[:state]
    @code = params[:code]
    @id_token = params[:id_token]
    @user = parmas[:user]

    # TODO: Validation

    render 'redirect'
  end
end

# redirect.html.erb
Redirect Completed

<h1>Code</h1>
<p><%= @code %></p>

<h1>State(<%= session[:state] %>)</h1>
<p><%= @state %></p>

<h1>ID TOKEN</h1>
<p><%= @id_token %></p>

<h1>User</h1>
<p><%= @user %></p>

Sign in again, but this time the redirect will be successful with four values code , state , user , and id_token .

r2IJnaz.png!web

Response Validation

At this point, we got everything we need to create an account. You can get sub (Apple user's unique ID) and user information in user , but don't just blindly trust everything from a network. You need to validate these information before use.

We use state to mitigate Cross-Site Request Forgery. We verify the state parameter by matches the one we sent at the beginning with the one we get back from the response.

def redirect
  @code = params[:code]

  if @code.present? && session[:state] == @state
    session.delete(:state)
    ...
  end
  ...
end

Full detail spec can be found here

ID Token (id_token) Validation

ID Tokenis a JSON Web Token (JWT) contain a set of user attributes, which are called claims .

Apple id_token contains following information:

// Header
{
  "kid": "AIDOPK1",
  "alg": "RS256"
}
// Payload
{
  "iss": "https://appleid.apple.com",
  "aud": [Services ID that we registered at the beginning],
  "exp": 1579073561,
  "iat": 1579072961,
  "sub": [Apple User Identifier],
  "c_hash": "Q4ZkNP4SB2f-m9vtLfO0UA",
  "email": [EMAIL],
  "email_verified": "true",
  "is_private_email": "true",
  "auth_time": 1579072961
}

I already show you how to validate JWT in my previous article, Sign in with Apple, Part 3: Backend – Token verification . Following are the fields you should validate:

Key Description Note iss (issuer) The issuer registered claim key. Should come from Apple ( https://appleid.apple.com in this case) aud (audience) The audience registered claim key. Must matched a Services ID ( client_id ) that you created inClient ID & Redirect URL` exp (expiration) The expiration time registered claim key. The current time MUST be before the time represented by the exp Claim iat (issued at) The issued at registered claim key, the value of which indicates the time at which the token was generated. You can check elapsed time since this issued time if you need custom expiration duration.

You must also validate the signature of the ID Token with Apple's public key. I already wrote a rough how-to in my previous post How to verify the token . You can check it out there.

Full detail spec can be found here

Token Request flow exchange Authorization Code ( code ) with access_token and id_token . As we always do, we need to validate the Authorization code before doing the exchange.

To do that, we need to compare the code we get with c_hash (Code Hash) value in the id_token . You can't just compare it, as a name imply Code Hash is a hash value of code. To compare it, you need to know what Code Hash represented.

c_hash Code hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the code value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is HS512, hash the code value with SHA-512, then take the left-most 256 bits and base64url encode them. The c_hash value is a case sensitive string.

If the ID Token is issued from the Authorization Endpoint with a code, which is the case for the response_type values code id_token and code id_token token, this is REQUIRED; otherwise, its inclusion is OPTIONAL.

https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken

To sum it up, these are steps you need to do:

c_hash

Full detail spec can be found here

Here is an example in Ruby:

c_hash = "xxx"
code = "yyy"
hash_code = Digest::SHA256.digest code
base64url_encode = Base64.urlsafe_encode64(hash_code[0, hash_code.size/2.to_i], padding: false)
    
if base64url_encode == c_hash
  # Valid
end

Exchange authorization code with an access token

The final part of the flow is to exchange authorization code for an access token. The endpoint and parameters are documents in Apple Documentation

Endpoint:

POST https://appleid.apple.com/auth/token

Parameters:

Key Description Note client_id The application identifier for your com.sarunw.siwa.client in my case. client_secret A secret generated as a JSON Web Token that uses the secret key generated by the WWDR portal. I will tell you how to get it in the code The authorization code received from your application’s user agent. The code is single use only and valid for five minutes. The one that we got from Authorization Response grant_type The grant type that determines how the client interacts with the server. For authorization code validation, use authorization_code . For refresh token validation requests, use refresh_token . Use authorization_code redirect_uri The destination URI the code was originally sent to.

At this point, we can fill in every parameter but one, client_secret . Let's see how to get that.

Create a Sign in with Apple private key

To get a client_secret , we need to create an Apple private key for Sign in with Apple service first.

  1. Go to Identifiers menu in Certificates, Identifiers & Profiles

  2. Choose Key

  3. Click the blue plus icon to register a new key. Give your key a name, and check the Sign In with Apple checkbox.

    Nfaqme3.png!web
  4. Click the Configure button and select the primary App ID you created earlier.

    vqIJna3.png!web
  5. Apple will generate a new private key for you and let you download it only once. Make sure you save this file because you won’t be able to get it back again later! The file you download will end in .p8. You also get key identifier ( kid ) (The key identifier appears below the key name.)

    UN7j2yU.png!web

Creating the Client Secret

Client secret is in JWT format with following header and payload:

// Header
{
  "kid": "[KEY_ID]",
  "alg": "ES256"
}
// Payload
{
  "iss": "[TEAM_ID]",
  "iat": 1579087819,
  "exp": 1594639819,
  "aud": "https://appleid.apple.com",
  "sub": "[CLIENT_ID]"
}

Header

Key Description Note alg The algorithm used to sign the token. ES256 in this case. kid A 10-character key identifier obtained from your developer account. We already got this Step 5. in the.

Payload

Key Description Note iss The issuer registered claim key, which has the value of your 10-character Team ID, obtained from your developer account. Log in to your Apple Developer Account and click on Membership section on the left panel, you will see your Team ID there. iat The issued at registered claim key, the value of which indicates the time at which the token was generated, in terms of the number of seconds since Epoch, in UTC. exp The expiration time registered claim key, the value of which must not be greater than 15777000 (6 months in seconds) from the Current Unix Time on the server. aud The audience registered claim key, the value of which identifies the recipient the JWT is intended for. Since this token is meant for Apple, use https://appleid.apple.com . Use https://appleid.apple.com sub The subject registered claim key, the value of which identifies the principal that is the subject of the JWT. Use the same value as client_id as this token is meant for your application Use a Services ID that we created inClient ID & Redirect URL

Here comes another tricky part. After you have everything in place, you need to sign in with the private key generated fromCreate a Sign in with Apple private key.

From Apple Documentation

After creating the token, sign it using the Elliptic Curve Digital Signature Algorithm (ECDSA) with the P-256 curve and the SHA-256 hash algorithm. Specify the value ES256 in the algorithm header key. Specify the key identifier in the kid attribute.

In my case, I use ruby-jwt

pem_content = <<~EOF
-----BEGIN PRIVATE KEY-----
xxxxx......
-----END PRIVATE KEY-----
EOF

ecdsa_key = OpenSSL::PKey::EC.new pem_content

headers = {
    'kid' => 'key_id'
}

claims = {
    'iss' => 'team_id',
    'iat' => Time.now.to_i,
    'exp' => Time.now.to_i + 86400*180,
    'aud' => 'https://appleid.apple.com',
    'sub' => 'client_id',
}

token = JWT.encode claims, ecdsa_key, 'ES256', headers

This is the JWT client secret.

token = JWT.encode claims, ecdsa_key, 'ES256', headers

This article should give you enough information for you to implement Sign in with Apple yourself (if you are a solo developer) or if you have a backed team, you should be able to find every value your colleagues would ask for.

I can't cover all the detail here (I provided a lot of references insection and footnote for further reading), but I think I cover all the basics you should know to implement Sign in with Apple in your application.

If you have any feedback, comment, or any mistakes on this series, you can contact me via email or Twitter; I would love to hear from you.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK