dev_note/TIL

OAuth2 State, Nonce 값 만들기

jjoylee 2022. 4. 25. 14:42

📌 정의

보통 SNS로그인을 할 때 AuthorizationEndpoint에 요청할 때 다음과 같은 두 값을 보낸다.

state : CSRF 대책으로서 동일한 세션임을 확인하기 위한 값. 각 요청에 대해 애플리케이션 측에서 생성한 임의의 문자열을 지정한다.

nonce : 리플레이 공격 대책을 위한 파라미터. 각 요청에 대해 임의의 문자열을 지정한다. 지정한 nonce 값은 ID Token에 포함되어 반환된다.

 

이를 C# OAuth2 개발할 때 다음과 같이 구현할 수 있다.

 

  1. 임의 바이트 값의 nonce를 생성하고 이를 쿠키에 저장한다. (GenerateCorrelationId)
  2. nonce와 다른 정보를 암호화 해서 state를 만들고 이를 쿼리 스트링으로 전달한다.
  3. 응답으로 받은 state 쿼리 스트링 파라미터를 복호화해서 cookie에 저장된 값과 일치하는지 검증한다. (ValidateCorrelationId)

  protected void GenerateCorrelationId(AuthenticationProperties properties)
  {
      if (properties == null)
      {
          throw new ArgumentNullException("properties");
      }

      string correlationKey = Constants.CorrelationPrefix + 
                                  BaseOptions.AuthenticationType;

      var nonceBytes = new byte[32];
      Random.GetBytes(nonceBytes);
      string correlationId = TextEncodings.Base64Url.Encode(nonceBytes);

      var cookieOptions = new CookieOptions
      {
          HttpOnly = true,
          Secure = Request.IsSecure
      };

      properties.Dictionary[correlationKey] = correlationId;

      Response.Cookies.Append(correlationKey, correlationId, cookieOptions);
  }

  protected bool ValidateCorrelationId(AuthenticationProperties properties, 
                                       ILogger logger)
  {
      if (properties == null)
      {
          throw new ArgumentNullException("properties");
      }

      string correlationKey = Constants.CorrelationPrefix + 
                                  BaseOptions.AuthenticationType;

      string correlationCookie = Request.Cookies[correlationKey];
      if (string.IsNullOrWhiteSpace(correlationCookie))
      {
          logger.WriteWarning("{0} cookie not found.", correlationKey);
          return false;
      }

      var cookieOptions = new CookieOptions
      {
          HttpOnly = true,
          Secure = Request.IsSecure
      };

      Response.Cookies.Delete(correlationKey, cookieOptions);

      string correlationExtra;
      if (!properties.Dictionary.TryGetValue(
          correlationKey,
          out correlationExtra))
      {
          logger.WriteWarning("{0} state property not found.", correlationKey);
          return false;
      }

      properties.Dictionary.Remove(correlationKey);

      if (!string.Equals(correlationCookie, correlationExtra, StringComparison.Ordinal))
      {
          logger.WriteWarning("{0} correlation cookie and state property mismatch.", 
                                  correlationKey);
          return false;
      }

      return true;
  }


  // REQUEST

  // nonce 생성
  GenerateCorrelationId(properties);
  // state 생성
  var state = Options.StateDataFormat.Protect(properties);

  // RESPONSE

  // state 값 쿼리스트링에서 가져오기
  var state = GetValue(query, "state");
  properties = Options.StateDataFormat.Unprotect(state);
  if (properties == null)
  {
      return null;
  }
  // 검증
  if (!ValidateCorrelationId(properties, _logger))
  {
      return new AuthenticationTicket(null, properties);
  }

📌 Reference

https://stackoverflow.com/questions/32768326/what-does-generatecorrelationid-and-validatecorrelationid-do