使用第三方服务进行自定义身份验证 #

用户进行身份验证时,有时用户的凭据和元数据可能存储在第三方服务中。在这些情况下,您可以连接到现有的第三方API来验证用户并检索其元数据。然后可以使用此元数据在Nakama中创建关联用户,从而有效地将其外部用户ID/用户名链接到Nakama用户。

本指南将演示两种场景,在这些场景中,使用Nakama的自定义身份验证功能允许对存储在第三方服务中的用户进行身份验证。

定制令牌交换身份验证 #

在本例中,我们的用户详细信息存储在第三方服务中,该服务公开了一个RESTful API,作为响应,我们可以向其传递ID并检索用户元数据。在自定义身份验证流程中,Nakama服务器将接收ID,将其传递给第三方API,然后从响应中提取用户的ID和用户名,使用此信息创建和/或验证Nakama用户。

我们将通过定义beforeAuthenticateCustom钩子来实现这一功能。该函数将使用配置的运行时环境变量来检索第三方API的主机名,并将传入in.Account.Id的内容发布到API进行验证。完成后,将使用新的用户ID和用户名将Nakama用户与其第三方用户账户详情相关联。

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
var (
	errMarshal = runtime.NewError("cannot marshal type", 13)
	errUnmarshal = runtime.NewError("cannot unmarshal type", 13)
	errApiPost = runtime.NewError("invalid API response", 13)
	errInternal = runtime.NewError("internal server error", 13)
)

func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
	// Register before hook on custom authentication
	err := initializer.RegisterBeforeAuthenticateCustom(beforeAuthenticateCustom)
	if err != nil {
		return fmt.Errorf("error registering before authentication hook: %s", err.Error())
	}

	return nil
}

func beforeAuthenticateCustom(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AuthenticateCustomRequest) (*api.AuthenticateCustomRequest, error) {
	// Get third-party API URL from the runtime context
	env := ctx.Value(runtime.RUNTIME_CTX_ENV).(map[string]string)
	apiUrl := env["AUTHENTICATION_API_URL"]

	// Construct a payload to send to the third-party API
	payload, err := json.Marshal(map[string]string {
		"id": in.Account.Id,
	})

	if err != nil {
		logger.Error("could not marshal api payload: %s", err.Error())
		return nil, errMarshal
	}

	// Send the HTTP Post request to the API
	/*
	Expected API Response
	HTTP 200
	{
		"user_id": "<UserId>",
		"username": "<Username>"
	}
	*/
	response, err := http.Post(apiUrl, "application/json", bytes.NewBuffer(payload))

	if err != nil {
		logger.Error("invalid API response: %s", err.Error())
		return nil, errApiPost
	}

	// Close the response when we're done
	defer response.Body.Close()

	// Read the response from the API and parse it as a map[string]string containing user info
	bytes, err := ioutil.ReadAll(response.Body)

	if err != nil {
		logger.Error("error reading API response body: %s", err.Error())
		return nil, errInternal
	}

	userInfo := make(map[string]string)
	err = json.Unmarshal(bytes, &userInfo)

	if err != nil {
		logger.Error("error unmarshaling API response: %s", err.Error())
		return nil, errUnmarshal
	}

	// Update the incoming authenticate request with the new user ID and username
	in.Account.Id = userInfo["user_id"]
	in.Username = userInfo["username"]

	return in, nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  initializer.registerBeforeAuthenticateCustom(BeforeAuthenticateCustom);
};

const BeforeAuthenticateCustom: nkruntime.BeforeHookFunction<nkruntime.AuthenticateCustomRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: nkruntime.AuthenticateCustomRequest): nkruntime.AuthenticateCustomRequest | void {
  const apiUrl = ctx.env['AUTHENTICATION_API_URL'];
  if (!apiUrl) {
    throw new Error('missing authentication api configuration');
  }

  // Construct a payload to send to the third-party API
  const payload = JSON.stringify({
    id: data.account.id
  });

  // Send the HTTP Post request to the API
  /*
  Expected API Response
  HTTP 200
  {
    "userId": "<UserId>",
    "username": "<Username>"  
  }
  */
  const response = nk.httpRequest(apiUrl, 'post', { 'content-type': 'application/json' }, JSON.stringify(payload));
  if (response.code > 299) {
    logger.error(`API error: ${response.body}`);
    return null
  }
  
  const userInfo = JSON.parse(response.body);
  if (!userInfo.userId || !userInfo.username) {
    logger.error(`invalid API response: ${response.body}`)
    return null;
  }
  
  // Update the incoming authenticate request with the new user ID and username
  data.account.id = userInfo.userId;
  data.username = userInfo.username;
  
  return data;
};
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

以上代码(针对适当的第三方API运行)将根据用户的第三方ID创建/验证Nakama用户。将使用此ID从第三方服务检索用户ID和用户名,并保存到Nakama用户账户中。用户的第三方用户ID将被链接到Nakama自定义身份验证ID属性。

JWT身份验证 #

JSON Web令牌(JWT)是传输用户元数据和相关用户声明/权限的常用方式。知道签署JWT使用的密钥的任何接受者都可以验证这些JWT。这非常有用,因为这意味着任何服务都可以验证特定用户的身份和权限,前提是该用户信任创建JWT的第三方服务。

在本示例中,我们的beforeAuthenticateCustom函数将使用运行时环境变量中指定的密钥从客户端接收并验证JWT。之后,从JWT中提取声明并使用它来验证Nakama中的用户,如前所述,将Nakama用户与其第三方用户ID和用户名相关联。您可以使用自己喜欢的JWT库来验证JWT的签名以及从其主体中提取数据。例如,jwt-go库。

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var (
	errJwtVerification = runtime.NewError("jwt verification failed", 3)
)

type Claims struct {
	Id       string `json:"id"`
	Username string `json:"username"`
}

err := initializer.RegisterBeforeAuthenticateCustom(beforeAuthenticateCustom)
if err != nil {
  return fmt.Errorf("error registering before authentication hook: %s", err.Error())
}

func beforeAuthenticateCustom(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AuthenticateCustomRequest) (*api.AuthenticateCustomRequest, error) {
	// Get the JWT secret key from the runtime context
	env := ctx.Value(runtime.RUNTIME_CTX_ENV).(map[string]string)
	secretKey := env["JWT_SECRET_KEY"]

	var claims Claims
	err := verifyAndParseJwt(secretKey, in.Account.Id, &claims)

	if err != nil {
		logger.Error("error verifying and parsing jwt: %s", err.Error())
		return nil, errJwtVerification
	}

	// Update the incoming authenticate request with the user ID and username from the JWT claims
	in.Account.Id = claims.Id
	in.Username = claims.Username

	return in, nil
}

func verifyAndParseJwt(secretKey string, jwt string, claims *Claims) error {
	// Use your favourite JWT library to verify the signature and decode the JWT contents (e.g. https://github.com/golang-jwt/jwt)
	// Once verified and decoded, populate the contents of the Claims object accordingly
	return nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface Claims {
  id: string,
  username: string
}

const BeforeAuthenticateCustom: nkruntime.BeforeHookFunction<nkruntime.AuthenticateCustomRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: nkruntime.AuthenticateCustomRequest): nkruntime.AuthenticateCustomRequest | void {
  const secretKey = ctx.env["JWT_SECRET_KEY"];
  const claims = verifyAndParseJwt(secretKey, data.account.id);

  if (!claims) {
    logger.error(`error verifying and parsing jwt`);
    return null;
  }

  // Update the incoming authenticate request with the user ID and username
  data.account.id = claims.id;
  data.username = claims.username;

  return data;
};

const verifyAndParseJwt = function (secretKey: string, jwt: string): Claims {
  // Use your favourite JWT library to verify the signature and decode the JWT contents
  // Once verified and decoded, return a Claims object accordingly
  return null;
}
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

通过以上两个示例可以看出,将第三方身份验证服务与Nakama集成是一个简单而灵活的过程,无论用户的信息存储在何处,都可以在Nakama中对用户成功地进行身份验证。