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

**URL:** https://heroiclabs.com/docs/kr/nakama/guides/concepts/custom-authentication/
**Summary:** 이 가이드는 제3자 인증 API를 통해 사용자 지정 인증을 사용하는 Nakama를 인증하는 방법에 대한 예시를 제공합니다.

---


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

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

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

## Bespoke 토큰 교환 인증

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

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

{{< code type="server" >}}
``` go
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
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
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 >}}

{{< missing type="server" lang="lua" / >}}

위의 코드는 (적절한 제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](https://github.com/dgrijalva/jwt-go) 라이브러리를 사용합니다.

{{< code type="server" >}}
```go
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
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
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 >}}

{{< missing type="server" lang="lua" / >}}

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