我从IOS Firebase API获取Apple Revoke Tokens Endpoint参数(client_id,client_secret,token)的位置在哪里?

19

苹果公司正在抱怨我的应用程序,因为我没有调用REST终端点revoke token来删除帐户。 我必须按照此文档中所述执行操作:https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens

为了调用此操作,我需要获取client_idclient_secrettoken。 我的应用程序中的登录流程由Firebase管理,当用户执行登录时,我不会保存这些信息。 因此,我需要从iOS上的Firebase auth中恢复这3个参数,以便调用revoke token终端点。

在Firebase auth API on IOS中可能有一种方法可以为我调用Apple终端点revoke_token,但我没有找到它。


当用户进行身份验证时,您会获得令牌。请查看此FB文档上的idTokenString https://firebase.google.com/docs/auth/ios/apple - Guilherme
@Guilherme,非常感谢您的回复。还有一个问题,如何验证撤销令牌API是否成功?在我们的测试中,似乎即使参数不正确,撤销令牌API也总是返回200。有关详细信息,请参见https://dev59.com/TcTra4cB1Zd3GeqP_pSJ?noredirect=1&lq=1。 - zangw
5
请将您找到的解决方案放在自己的答案中,而不是将其添加到问题中。我已经撤销了添加解决方案到问题中的编辑。 - Ryan M
1
@zangw,我理解你是想生成client_secret而不是获取用户令牌,对吗?我从Apple下载了Authkey(使用Apple登录)。在那里,你可以获得生成client_secret令牌的密钥。我用Java从这段代码中创建了它:https://dev59.com/bMHqa4cB1Zd3GeqPzGvq#68380631 你可以看到,已删除了断行和“头文件和尾文件”。 - Guilherme
1
关于始终获得200作为响应,我在Postman中发送了错误的数据,这是真的,始终会获得200。但是我使用此帖子中描述的所有内容将我的应用程序提交进行验证,Apple接受了我的应用程序并从Ap Connect中删除了红色警告。我认为,如果您生成正确的client_secret并发送有效的用户令牌以及您的有效client_id,则一切都将变得正确。 - Guilherme
显示剩余9条评论
3个回答

9

在Firebase中撤销苹果登录的Token

本文将介绍如何在Firebase环境中撤销使用“苹果登录”方式登录的Token。
根据苹果审核指南,未在2022年6月30日之前采取行动的应用可能会被删除。
翻译人员为本文编写了翻译稿,如果您对这些句子感到奇怪或描述不准确,请原谅。
本文使用Firebase的Functions,如果Firebase将来提供相关函数,我建议使用它。

整个过程如下所示。

  1. 从用户登录的应用程序中获取 authorizationCode。
  2. 使用具有过期时间的 authorizationCode 获取无过期时间的刷新令牌。
  3. 保存刷新令牌后,在用户离开服务时撤销该令牌。

您可以在 https://appleid.apple.com/auth/token 获取刷新令牌,并在 https://appleid.apple.com/auth/revoke 撤销令牌。

开始

如果您已经使用Firebase实现了“苹果登录”,则应该在项目中某个位置有ASAuthorizationAppleIDCredential。
在我的情况下,它写成以下形式。

  func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
      guard let nonce = currentNonce else {
        fatalError("Invalid state: A login callback was received, but no login request was sent.")
      }
      guard let appleIDToken = appleIDCredential.identityToken else {
        print("Unable to fetch identity token")
        return
      }
      guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
        print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
        return
      }
      // Initialize a Firebase credential.
      let credential = OAuthProvider.credential(withProviderID: "apple.com",
                                                IDToken: idTokenString,
                                                rawNonce: nonce)
      // Sign in with Firebase.
      Auth.auth().signIn(with: credential) { (authResult, error) in
        if error {
          // Error. If error.code == .MissingOrInvalidNonce, make sure
          // you're sending the SHA256-hashed nonce as a hex string with
          // your request to Apple.
          print(error.localizedDescription)
          return
        }
        // User is signed in to Firebase with Apple.
        // ...
      }
    }
  }

我们需要的是授权码authorizationCode。将以下代码添加到获取idTokenString的 guard 下方即可。
...

guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
  print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
  return
}

// Add new code below
if let authorizationCode = appleIDCredential.authorizationCode,
   let codeString = String(data: authorizationCode, encoding: .utf8) {
    print(codeString)
}

...


一旦您达到这个阶段,当用户登录时,可以获得授权码authorizationCode。
但是,我们需要通过authorizationCode获取刷新令牌refresh token,此操作需要JWT,因此让我们使用Firebase函数来实现。
暂停Xcode一会儿,并转到Firebase函数中的代码。
如果您从未使用过函数,请参考https://firebase.google.com/docs/functions
在Firebase函数中,您可以使用JavaScript或TypeScript,我使用了JavaScript。
首先,让我们声明一个全局创建JWT的函数。使用npm install安装所需的软件包。
有一个地方可以编写您的密钥文件和ID(团队、客户端、密钥)路由,因此请编写自己的信息。
如果您不知道自己的ID信息,请参考相关问题。https://github.com/jooyoungho/apple-token-revoke-in-firebase/issues/1
function makeJWT() {

  const jwt = require('jsonwebtoken')
  const fs = require('fs')

  // Path to download key file from developer.apple.com/account/resources/authkeys/list
  let privateKey = fs.readFileSync('AuthKey_XXXXXXXXXX.p8');

  //Sign with your team ID and key ID information.
  let token = jwt.sign({ 
  iss: 'YOUR TEAM ID',
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + 120,
  aud: 'https://appleid.apple.com',
  sub: 'YOUR CLIENT ID'
  
  }, privateKey, { 
  algorithm: 'ES256',
  header: {
  alg: 'ES256',
  kid: 'YOUR KEY ID',
  } });
  
  return token;
}

上述函数是通过使用您的密钥信息创建JWT而返回的。
现在,让我们使用AuthorizationCode获取Refresh Token。
我们将向函数添加名为getRefreshToken的函数。

exports.getRefreshToken = functions.https.onRequest(async (request, response) => {

    //import the module to use
    const axios = require('axios');
    const qs = require('qs')

    const code = request.query.code;
    const client_secret = makeJWT();

    let data = {
        'code': code,
        'client_id': 'YOUR CLIENT ID',
        'client_secret': client_secret,
        'grant_type': 'authorization_code'
    }
    
    return axios.post(`https://appleid.apple.com/auth/token`, qs.stringify(data), {
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
    },
    })
    .then(async res => {
        const refresh_token = res.data.refresh_token;
        response.send(refresh_token);
        
    });

});

当您调用上述函数时,会从查询中获取代码并获得一个refresh_token。 对于code而言,这是我们从应用程序中获取的授权码。 在连接到应用程序之前,让我们也添加一个revoke函数。

exports.revokeToken = functions.https.onRequest( async (request, response) => {

  //import the module to use
  const axios = require('axios');
  const qs = require('qs');

  const refresh_token = request.query.refresh_token;
  const client_secret = makeJWT();

  let data = {
      'token': refresh_token,
      'client_id': 'YOUR CLIENT ID',
      'client_secret': client_secret,
      'token_type_hint': 'refresh_token'
  };

  return axios.post(`https://appleid.apple.com/auth/revoke`, qs.stringify(data), {
      headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
      },
  })
  .then(async res => {
      console.log(res.data);
  });
});

以上函数基于我们获取的refresh_token撤销登录信息。
到目前为止,我们已经配置好了我们的函数。当我们执行“firebase deploy functions”时,我们将在Firebase函数控制台中添加一些内容。

img

现在回到Xcode。
在之前编写用于保存Refresh token的代码中调用Functions地址。
我将其保存在UserDefaults中,您也可以将其保存在Firebase数据库中。

...

// Add new code below
if let authorizationCode = appleIDCredential.authorizationCode, let codeString = String(data: authorizationCode, encoding: .utf8) {
              
      let url = URL(string: "https://YOUR-URL.cloudfunctions.net/getRefreshToken?code=\(codeString)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "https://apple.com")!
            
        let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
            
            if let data = data {
                let refreshToken = String(data: data, encoding: .utf8) ?? ""
                print(refreshToken)
                UserDefaults.standard.set(refreshToken, forKey: "refreshToken")
                UserDefaults.standard.synchronize()
            }
        }
      task.resume()
      
  }

...

在此时,当用户登录时,其设备将把refresh_token保存为UserDefaults。现在剩下的就是在用户离开服务时撤销。

  func removeAccount() {
    let token = UserDefaults.standard.string(forKey: "refreshToken")

    if let token = token {
      
        let url = URL(string: "https://YOUR-URL.cloudfunctions.net/revokeToken?refresh_token=\(token)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "https://apple.com")!
              
        let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
          guard data != nil else { return }
        }
              
        task.resume()
        
    }
    ...
    //Delete other information from the database...
    FirebaseAuthentication.shared.signOut()
  }
        

如果我们到目前为止都按照要求进行了操作,那么我们的应用程序应该已从“设置 - 密码与安全性 - 使用 Apple ID 的应用程序”中移除。
谢谢。

苹果的appleIdToken是否与苹果登录响应的identityToken相同? - zangw
@zangw 是的,它与“使用Apple登录”委托方法被调用时提供的identityToken相同。 - Paresh Patel
1
@PareshPatel,我们使用auth/token的访问令牌成功地创建了撤销令牌API。详细信息请参见https://dev59.com/TcTra4cB1Zd3GeqP_pSJ#72656409。 - zangw
由于API的响应不明确,无法检查令牌是否正确。正如@zangw所说,我认为您需要发送正确的令牌。 - Youngho Joo
@algrid 在登录时,您必须使用有效期为10分钟的代码获取refresh_token。我们会及时整理和更新内容。 - Youngho Joo
显示剩余6条评论

3

官方支持的 Firebase 比我的答案更准确、更方便。我们希望在苹果指导文件发布之前尽快获得支持。 - Youngho Joo
@CalHoll,他们什么时候会实现这个功能,有任何更新吗? - Dennis Ashford
他们现在说,由于需要团队进行安全审计,这将要等到第四季度才能完成。与此同时,这里推荐了一个函数解决方案:https://github.com/jooyoungho/apple-token-revoke-in-firebase - Calholl

0

我认为这应该从后端完成,以避免将敏感数据(client_secret)暴露给应用程序。以下是我在 .net 中生成 client_secret 并调用撤销令牌 API 端点的方法:

public static class EndUserUtils
{

    //-------------------------- Apple JWT --------------------------
    //Must add System.IdentityModel.Tokens.Jwt from NUGet

    using System.Security.Claims;
    using System.Security.Cryptography;
    
    public static string GetAppleJWTToken(IErrorLogService errorLogService)
    {
        var dsa = GetECDsa(errorLogService);
        return dsa != null ? CreateJwt(dsa, "KEY_ID", "TEAM_ID") : null; //Get KEY_ID and TEAM_ID from Apple developer site
    }

    private static ECDsa GetECDsa(IErrorLogService errorLogService)
    {
        try
        {
            var keyPath = Path.Combine("..", "Settings", "Keys", "AuthKey_KEY_ID.p8"); //Download from apple developer
            using (TextReader reader = System.IO.File.OpenText(keyPath))
            {
                var privateKey = reader.ReadToEnd();
                privateKey = privateKey
                    .Replace("-----BEGIN PRIVATE KEY-----", "")
                    .Replace("-----END PRIVATE KEY-----", "")
                    .Replace("\n", "");
                var ecdsa = ECDsa.Create();
                ecdsa?.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);
                return ecdsa;
            }
        }
        catch (Exception ex)
        {
            errorLogService?.AddException(ex);
        }
        return null;
    }

    private static string CreateJwt(ECDsa key, string keyId, string teamId)
    {
        var securityKey = new ECDsaSecurityKey(key) { KeyId = keyId };
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);

        var descriptor = new SecurityTokenDescriptor
        {
            IssuedAt = DateTime.UtcNow,
            Issuer = teamId,
            SigningCredentials = credentials,
            Expires = DateTime.UtcNow.AddMinutes(5), //Define how long generated JWT will be valid
            Audience = "https://appleid.apple.com",
            Subject = new ClaimsIdentity(new[]
            {
                new Claim("sub", "com.example.appname") //APP_ID 
            })
        };

        var handler = new JwtSecurityTokenHandler();
        var encodedToken = handler.CreateEncodedJwt(descriptor);
        return encodedToken;
    }
}

从 .net core 后端调用苹果的 '撤销' 令牌终点

//Only for SignIn with Apple
if (!string.IsNullOrEmpty(tokenToRevoke))
{
    var secret = EndUserUtils.GetAppleJWTToken(_errorLogService);
    if (secret != null)
    {
        var formData = new List<KeyValuePair<string, string>>();
        formData.Add(new KeyValuePair<string, string>("client_id", "com.example.appname"));
        formData.Add(new KeyValuePair<string, string>("client_secret", secret));
        formData.Add(new KeyValuePair<string, string>("token", tokenToRevoke));
        formData.Add(new KeyValuePair<string, string>("token_type_hint", "access_token"));

        var request = new HttpRequestMessage(HttpMethod.Post, "https://appleid.apple.com/auth/revoke")
        {
            Content = new FormUrlEncodedContent(formData)
        };

        using (var client = _httpClientFactory.CreateClient())
        {
            var result = client.SendAsync(request).GetAwaiter().GetResult();

            if (!result.IsSuccessStatusCode)
            {
                _errorLogService.AddError($"Error revoking Apple idToken: {result.StatusCode}, {result.Content}");
                
                //return error to application
            }
        }
    }
}

1
您可以在设备本身上生成凭据,因此在这种情况下,在后端执行撤销操作实际上是没有意义的。特别是对于Firebase而言,它不知道您是如何生成凭据的,只知道它已被Apple批准为合法。 - KLD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接