제3자 서비스를 통한 사용자 지정 인증 #

사용자 인증 시 자격 인증과 메타데이터가 제3자 서비스로 저장되는 경우도 있습니다. 이 경우, 기존의 제3자 API를 사용하여 사용자에 대한 유효성 검사를 실시하고 메타데이터를 회수할 수 있습니다. 이 메타데이터를 사용하여 Nakama에서 연관된 사용자를 생성하여 외부 사용자 ID / 사용자 이름을 Nakama 사용자로 효율적으로 연결할 수 있습니다.

이 가이드에서는 Nakama의 사용자 지정 인증 기능을 사용하여 제3자 서비스에 저장된 사용자 인증을 활성화하는 두 가지 시나리오에 대해서 설명합니다.

Bespoke 토큰 교환 인증 #

이 예시에서는 RESTful API를 노출시키는 제3자 서비스에 저장된 사용자의 세부 내역을 사용하여 ID를 전달하고 사용자의 메타데이터를 회수합니다. Nakama 서버에는 사용자 지정 인증의 일부로 ID가 제3자 API로 전달되어 사용자의 ID와 사용자 이름을 추출하고, 해당 정보를 통해 Nakama 사용자를 생성하고 인증을 진행합니다.

beforeAuthenticateCustom 후크를 정의하여 이 작업을 완료할 수 있습니다. 이 기능은 구성된 런타임 환경 변수를 사용하여 제3자 API의 호스트 이름을 회수하고 검증을 위해서 in.Account.Id을(를) API로 전달합니다. 작업이 완료되면, 새로운 사용자 ID와 사용자 이름을 통해 Nakama 사용자를 제3자 사용자 계정 세부내역과 연계시킬 수 있습니다.

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.

위의 코드는 (적절한 제3자 API로 실행할 경우) 제3자 ID를 통해 Nakama 사용자를 생성/인증합니다. 이 ID를 통해 제3자 서비스에서 사용자 ID와 사용자 이름을 회수하고 Nakama 사용자 계정으로 저장합니다. 제3자 사용자 ID는 Nakama 사용자 지정 인증 ID 속성과 연결됩니다.

JWT 인증 #

JSON 웹 토큰(JWT)은 사용자 메타데이터와 연관된 사용자 요청/권한을 전달할 수 있는 일반적인 방법입니다. JWT는 서명에 필요한 비밀 키를 알고 있는 모든 사람이 인증할 수 있습니다. JWT를 생성한 제3자 서비스를 신뢰한다고 가정할 경우, 모든 서비스가 특정 사용자의 신원과 권한을 검증할 수 있기 때문에 매우 유용합니다.

이 예시에서는 beforeAuthenticateCustom 기능이 런타임 환경 변수에서 지정된 비밀 키를 사용하여 클라이언트에서 JWT를 검증합니다. 그 다음, JWT에서 요청 사항을 추출하고 Nakama에서 사용자를 인증하여 Nakama 사용자를 이전과 같이 제3자 사용자 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.

두 가지 예시를 통해서 보셨듯이, 제3자 인증 서비스를 Nakama와 통합하는 것은 정보가 어디에 저장되어 있든 Nakama에서 사용자 인증을 성공적으로 수행할 수 있는 매우 간단하고 유연한 방식입니다.