Back to blog

PostgreSQL 18 OAuth2 OAUTHBEARER ν΄λΌμ΄μ–ΈνŠΈ κ΅¬ν˜„ 방법 (3)

August 10, 2026

μž‘μ„±μž: EDB Korea Team
2025λ…„ 9μ›” 19일

PostgreSQL 18 OAuth2 ν΄λΌμ΄μ–ΈνŠΈ κ΅¬ν˜„ 방식

PostgreSQL 18은 OAuth2 인증을 λ„μž…ν•΄ κΈ°μ‘΄ μ‚¬μš©μžλͺ…κ³Ό λΉ„λ°€λ²ˆν˜Έ μ‘°ν•© λŒ€μ‹  OAuth2 ν† ν°μœΌλ‘œ μ‚¬μš©μžλ₯Ό 인증할 수 μžˆλ„λ‘ μ§€μ›ν•©λ‹ˆλ‹€. 이 κ°€μ΄λ“œμ—μ„œλŠ” 널리 μ‚¬μš©λ˜λŠ” pgx Go λ“œλΌμ΄λ²„λ₯Ό μ°Έμ‘° κ΅¬ν˜„μœΌλ‘œ ν™œμš©ν•΄ PostgreSQL ν΄λΌμ΄μ–ΈνŠΈ λΌμ΄λΈŒλŸ¬λ¦¬μ— OAuth Bearer 토큰 인증을 κ΅¬ν˜„ν•˜λŠ” 방법을 μ‚΄νŽ΄λ΄…λ‹ˆλ‹€.

이 μ‹œλ¦¬μ¦ˆλŠ” 총 3편으둜 κ΅¬μ„±λ©λ‹ˆλ‹€.

이 κΈ€μ—μ„œ μ‚¬μš©ν•˜λŠ” μ½”λ“œλŠ” ν•„μžμ˜ pgx 포크인 xugy99/pgxλ₯Ό 기반으둜 ν•©λ‹ˆλ‹€.


PostgreSQL 18의 OAUTHBEARER 인증 ꡬ쑰

OAUTHBEARER SASL λ©”μ»€λ‹ˆμ¦˜μ„ μ‚¬μš©ν•˜λ©΄ ν΄λΌμ΄μ–ΈνŠΈκ°€ OAuth 2.0 Bearer ν† ν°μœΌλ‘œ 인증할 수 μžˆμŠ΅λ‹ˆλ‹€. PostgreSQL 18은 κΈ°μ‘΄ SASL 인증 흐름에 OAUTHBEARER 인증을 ν†΅ν•©ν–ˆμœΌλ©°, 전체 OAUTHBEARER SASL 흐름은 이 μ‹œλ¦¬μ¦ˆμ˜ Part-1μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.


pgx의 PostgreSQL μ—°κ²° 및 인증 ꡬ쑰

pgxλŠ” ConnectWithOptions() ν•¨μˆ˜λ₯Ό 톡해 PostgreSQL ν΄λŸ¬μŠ€ν„° URLκ³Ό μ˜΅μ…˜μ„ λ°›μ•„ μƒˆλ‘œμš΄ 연결을 μƒμ„±ν•©λ‹ˆλ‹€. 이λ₯Ό μ΄μš©ν•΄ 쿼리λ₯Ό μ‹€ν–‰ν•˜λŠ” κΈ°λ³Έ μ˜ˆμ‹œλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

func main() {
   // ... ...
   url := "postgres://joe:joespassword@localhost:5432/my_db"
   conn, err = pgx.ConnectWithOptions(context.Background(), url, opts)
   if err != nil {
       log.Fatalf("Unable to connect to database: %v\n", err)
   }
   // run sql
   rows, err := conn.Query(context.Background(), *sql)
   if err != nil {
       log.Fatalf("Failed to execute SQL: %v\n", err)
   }
   defer rows.Close()
   // collect rows
   // ... ...
}

μœ„ μ˜ˆμ‹œμ˜ urlμ—λŠ” κΈ°μ‘΄ λ°©μ‹μ˜ μ‚¬μš©μžλͺ…κ³Ό λΉ„λ°€λ²ˆν˜Έκ°€ ν¬ν•¨λ˜μ–΄ 있으며, ν•¨μˆ˜ ν˜ΈμΆœμ„ 톡해 이후 쿼리에 μ‚¬μš©ν•  수 μžˆλŠ” pgconn 객체λ₯Ό λ°˜ν™˜λ°›μŠ΅λ‹ˆλ‹€.

pgx.ConnectWithOptions()λŠ” 크게 μ—°κ²° λ¬Έμžμ—΄ νŒŒμ‹±, 인증 방식 선택, μ—°κ²° 수립 과정을 κ±°μΉ©λ‹ˆλ‹€. 각 λ‹¨κ³„μ˜ 처리 방식을 μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

1. PostgreSQL μ—°κ²° λ¬Έμžμ—΄ νŒŒμ‹±

//
// from pgconn/pgconn.go in github.com/xugy99/pgx
//
func ConnectWithOptions(ctx context.Context, connString string, options ParseConfigOptions) (*Conn, error) {
   connConfig, err := ParseConfigWithOptions(connString, options)
   if err != nil {
       return nil, err
   }
   return connect(ctx, connConfig)
}

pgconn/config.go의 ParseConfigWithOptions()λŠ” λ‹€μŒ 두 κ°€μ§€ μ—°κ²° λ¬Έμžμ—΄ ν˜•μ‹μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€.

  • URL ν˜•μ‹: postgres://user:pass@host:port/db?param=value β†’ parseURLSettings()둜 νŒŒμ‹±
  • ν‚€μ›Œλ“œ/κ°’ ν˜•μ‹: host=localhost port=5432 user=myuser β†’ parseKeywordValueSettings()둜 νŒŒμ‹±

핡심 νŒŒμ‹± λ‘œμ§μ€ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

//
// from pgconn/config.go in github.com/xugy99/pgx
//
if strings.HasPrefix(connString, "postgres://") || strings.HasPrefix(connString, "postgresql://") {
   connStringSettings, err = parseURLSettings(connString)
} else {
   connStringSettings, err = parseKeywordValueSettings(connString)
}

2. μ„œλ²„ 응닡에 λ”°λ₯Έ 인증 방식 선택

TCP/TLS 연결을 μˆ˜λ¦½ν•œ ν›„ connectOne()은 μŠ€νƒ€νŠΈμ—… λ©”μ‹œμ§€λ₯Ό 보내고 μ„œλ²„μ˜ 인증 μš”μ²­μ„ κΈ°λ‹€λ¦½λ‹ˆλ‹€. pgconn/pgconn.go의 connectOne()은 μ„œλ²„κ°€ λ°˜ν™˜ν•œ λ©”μ‹œμ§€ μœ ν˜•μ— 따라 인증 방식을 μ„ νƒν•©λ‹ˆλ‹€.

//
// from pgconn/pgconn.go in github.com/xugy99/pgx
//
func connectOne(ctx context.Context, config *Config, connectConfig *connectOneConfig,
    ignoreNotPreferredErr bool,
) (*PgConn, error) {
   // ... ...
   switch msg := msg.(type) {
   case *pgproto3.AuthenticationOk:
       // No auth needed
   case *pgproto3.AuthenticationCleartextPassword:
       err = pgConn.txPasswordMessage(pgConn.config.Password)
   case *pgproto3.AuthenticationMD5Password:
       digestedPassword := "md5" + hexMD5(hexMD5(pgConn.config.Password+pgConn.config.User)+string(msg.Salt[:]))
       err = pgConn.txPasswordMessage(digestedPassword)
   case *pgproto3.AuthenticationSASL:
       err = pgConn.scramAuth(msg.AuthMechanisms)
       if err != nil {
           pgConn.conn.Close()
           return nil, newPerDialConnectError("failed SASL auth", err)
       }
   case *pgproto3.AuthenticationGSS:
       err = pgConn.gssAuth()  // GSSAPI/Kerberos
   }
   // ... ...
}

μ„œλ²„κ°€ AuthenticationOkλ₯Ό λ°˜ν™˜ν•˜λ©΄ 인증이 μ™„λ£Œλ˜κ³  연결은 connStatusIdle μƒνƒœλ‘œ μ „ν™˜λ©λ‹ˆλ‹€.

μ—¬κΈ°μ„œ 확인해야 ν•  뢀뢄은 pgproto3.AuthenticationSASL λΆ„κΈ°μž…λ‹ˆλ‹€. ν˜„μž¬ pgxμ—μ„œ SASL 기반으둜 κ΅¬ν˜„λœ 인증 방식은 SCRAM(Salted Challenge Response Authentication Mechanism)이며, ν΄λΌμ΄μ–ΈνŠΈκ°€ AuthenticationSASL λ©”μ‹œμ§€λ₯Ό λ°›μœΌλ©΄ SCRAM 인증 흐름을 μ‹œμž‘ν•©λ‹ˆλ‹€.

3. SCRAM 기반 SASL 인증 흐름

PostgreSQL은 SCRAM-SHA-256κ³Ό SCRAM-SHA-256-PLUSλ₯Ό μ§€μ›ν•©λ‹ˆλ‹€. 기본적인 ν•Έλ“œμ…°μ΄ν¬ 흐름은 λ‹€μŒ λ‹€μ΄μ–΄κ·Έλž¨μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

PostgreSQL 18 SCRAM in SASL

μ•žμ„œ μ‚΄νŽ΄λ³Έ μ—°κ²° μ½”λ“œλ₯Ό λ‹€μ‹œ ν™•μΈν•˜λ©΄ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

//
// from pgconn/pgconn.go in github.com/xugy99/pgx
//
func connectOne(ctx context.Context, config *Config, connectConfig *connectOneConfig,
    ignoreNotPreferredErr bool,
) (*PgConn, error) {
   // ... ...
   // ... ...
   case *pgproto3.AuthenticationSASL:
       err = pgConn.scramAuth(msg.AuthMechanisms)
       if err != nil {
           pgConn.conn.Close()
           return nil, newPerDialConnectError("failed SASL auth", err)
       }
   // ... ...
   // ... ...
}

msg.AuthMechanismsλŠ” 인증 λ©”μ»€λ‹ˆμ¦˜(SCRAM-SHA-256)을 μ§€μ •ν•˜λ©°, pgConn.scramAuth()λŠ” λ‹€μŒ λ„€ κ°€μ§€ λ©”μ‹œμ§€λ₯Ό μ΄μš©ν•΄ SASL 인증 흐름을 μ²˜λ¦¬ν•©λ‹ˆλ‹€.

  • SASLInitialResponse
  • AuthenticationSASLContinue
  • SASLResponse
  • AuthenticationSASLFinal

pgx에 OAUTHBEARER 인증 κ΅¬ν˜„ν•˜κΈ°

κΈ°μ‘΄ SCRAM 인증 처리 ꡬ쑰λ₯Ό ν™œμš©ν•˜λ©΄ AuthenticationSASL λ©”μ‹œμ§€μ—μ„œ OAUTHBEARER 지원 μ—¬λΆ€λ₯Ό ν™•μΈν•˜κ³  OAuth2 μ „μš© 인증 ν•¨μˆ˜λ‘œ μ—°κ²°ν•˜λ„λ‘ ν™•μž₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

ν΄λΌμ΄μ–ΈνŠΈλŠ” SASLInitialResponseμ—μ„œ auth=""을 보내 OIDC λ””μŠ€μ»€λ²„λ¦¬λ₯Ό μš”μ²­ν•˜κ³  authorize μ—”λ“œν¬μΈνŠΈλ₯Ό 톡해 OAuth2 둜그인 흐름을 μ™„λ£Œν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” PostgreSQL 18의 OAuth2 인증 흐름을 보닀 μ™„μ „ν•˜κ²Œ κ΅¬ν˜„ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€. 이 κΈ€μ—μ„œλŠ” 이 단계λ₯Ό μƒλž΅ν•˜κ³  이미 토큰을 ν™•λ³΄ν–ˆλ‹€κ³  κ°€μ •ν•œ λ’€ SASLInitialResponse에 토큰을 직접 포함해 μ „μ†‘ν•˜λŠ” 방식을 μ‚¬μš©ν•©λ‹ˆλ‹€. κ΅¬ν˜„ν•  흐름은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

PostgreSQL 18 and PGX Demo

1. OAUTHBEARER 인증 흐름 κ΅¬ν˜„

핡심 OAuth 인증 λ‘œμ§μ€ μ—°κ²° 수립 과정에 κ΅¬ν˜„ν•©λ‹ˆλ‹€.

//
// from pgconn/auth_oauth.go in github.com/xugy99/pgx
//
func (c *PgConn) oauthAuth() error {
   // Check if we have a pre-configured bearer token
   if c.config == nil || c.config.OAuthBearerToken == "" {
       return fmt.Errorf("server requires OAuthBearerToken for this connection")
   }
   // Construct a SASLInitialResponse message
   reply := &pgproto3.SASLInitialResponse{
       AuthMechanism: "OAUTHBEARER",
       Data:          buildOAuthInitialResponse(c.config.OAuthBearerToken),
   }
   // Use frontend.Send() to populate the send buffer and flush it to the wire
   c.frontend.Send(reply)
   if err := c.flushWithPotentialWriteReadDeadlock(); err != nil {
       return err
   }
   // Wait for AuthenticationOk message
   _, err := c.rxOAuthSASLOk()
   return err
}

이 κ΅¬ν˜„μ—μ„œ 확인할 μ£Όμš” 사항은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • 토큰 μ£Όμž…: 토큰은 μ „λ‹¬λœ pgconn μ—°κ²°μ˜ configμ—μ„œ κ°€μ Έμ˜΅λ‹ˆλ‹€. 이후 λ‹¨κ³„μ—μ„œ config 객체에 토큰을 μ „λ‹¬ν•˜λŠ” 방법을 μ‚΄νŽ΄λ΄…λ‹ˆλ‹€.
  • SASL 톡합: κΈ°μ‘΄ SASL λ©”μ‹œμ§€ ꡬ쑰λ₯Ό μž¬μ‚¬μš©ν•˜λ©°, Data νŽ˜μ΄λ‘œλ“œλŠ” buildOAuthInitialResponse() ν•¨μˆ˜μ—μ„œ κ΅¬μ„±ν•©λ‹ˆλ‹€.
  • 응닡 처리: rxOAuthSASLOk()λŠ” PostgreSQL μ„œλ²„μ˜ 응닡을 기닀리고 AuthenticationOk λ©”μ‹œμ§€λ₯Ό ν™•μΈν•©λ‹ˆλ‹€.

2. OAuth 초기 응닡 ꡬ성

λ‹€μŒμœΌλ‘œ RFC 7628에 λ§žλŠ” OAuth 초기 응닡을 κ΅¬μ„±ν•˜κΈ° μœ„ν•΄ buildOAuthInitialResponse() ν•¨μˆ˜λ₯Ό κ΅¬ν˜„ν•©λ‹ˆλ‹€.

RFC 7628 μ„Ήμ…˜ 4.1의 Bearer 토큰 κ΅ν™˜ μ˜ˆμ‹œλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

[Initial connection and TLS establishment...]
S: * OK IMAP4rev1 Server Ready
C: t0 CAPABILITY
S: * CAPABILITY IMAP4rev1 AUTH=OAUTHBEARER SASL-IR
S: t0 OK Completed
C: t1 AUTHENTICATE OAUTHBEARER bixhPXVzZXJAZXhhbXBsZS5jb20sAWhv
c3Q9c2VydmVyLmV4YW1wbGUuY29tAXBvcnQ9MTQzAWF1dGg9QmVhcmVyI
HZGOWRmdDRxbVRjMk52YjNSbGNrQmhiSFJoZG1semRHRXVZMjl0Q2c9PQ
EB
S: t1 OK SASL authentication succeeded
As required by IMAP [RFC3501], the payloads are base64 encoded. The
decoded initial client response (with %x01 represented as ^A and long
lines wrapped for readability) is:
n,a=user@example.com,^Ahost=server.example.com^Aport=143^A
auth=Bearer vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==^A^A

 

PostgreSQL 18의 libpq κ΅¬ν˜„(backend/libpq/auth-oauth.c)μ—μ„œλŠ” λ‹€μŒκ³Ό 같이 μ²˜λ¦¬ν•©λ‹ˆλ‹€.

static int
oauth_exchange(void *opaq, const char *input, int inputlen,
              char **output, int *outputlen, const char **logdetail)
{
   // ... ...
   //

   /* All remaining fields are separated by the RFC's kvsep (\x01). */
   if (*p != KVSEP)
       ereport(ERROR,
               errcode(ERRCODE_PROTOCOL_VIOLATION),
               errmsg("malformed OAUTHBEARER message"),
               errdetail("Key-value separator expected, but found character \"%s\".",
                           sanitize_char(*p)));
   p++;
   auth = parse_kvpairs_for_auth(&p);
   if (!auth)
       ereport(ERROR,
               errcode(ERRCODE_PROTOCOL_VIOLATION),
               errmsg("malformed OAUTHBEARER message"),
               errdetail("Message does not contain an auth value."));
   }
   // ... ...
   //
}

RFC 7628 및 PostgreSQL 18 μš”κ΅¬μ‚¬ν•­μ„ μΆ©μ‘±ν•˜λ €λ©΄ λ‹€μŒ ν˜•μ‹μ„ κ΅¬ν˜„ν•΄μ•Ό ν•©λ‹ˆλ‹€.

  • ν˜•μ‹: n,,^Aauth=Bearer <token>^A^A
  • κΆŒν•œ λΆ€μ—¬ ID: n,은 authorization identityκ°€ μ—†μŒμ„ 의미
  • ν‚€-κ°’ ꡬ뢄: ^A(0x01)둜 key-value μŒμ„ ꡬ뢄
  • 토큰 ν˜•μ‹: auth=Bearer <token>은 OAuth 2.0 Bearer 토큰 ν˜•μ‹μ„ λ”°λ₯΄λ©°, μ›μ‹œ 토큰을 base64둜 인코딩해야 ν•©λ‹ˆλ‹€.
  • ^A=0x01이며 host/portλŠ” RFC 7628에 따라 ν•„μˆ˜κ°€ μ•„λ‹™λ‹ˆλ‹€.

이λ₯Ό λ°”νƒ•μœΌλ‘œ κ΅¬ν˜„ν•œ ν•¨μˆ˜λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

//
// from pgconn/auth_oauth.go in github.com/xugy99/pgx
//
func buildOAuthInitialResponse(token string) []byte {
   var b bytes.Buffer
   b.WriteString("n,,")           // No authorization identity
   b.WriteByte(0x01)              // Key-value separator
   b.WriteString("auth=Bearer ")  // OAuth Bearer token prefix
   b.WriteString(token)           // Actual token
   b.WriteByte(0x01)              // Key-value separator
   b.WriteByte(0x01)              // Final separator
   return b.Bytes()
}

3. AuthenticationOk 응닡 처리

rxOAuthSASLOk() ν•¨μˆ˜λŠ” PostgreSQL μ„œλ²„μ—μ„œ λ‹€μŒ 응닡 λ©”μ‹œμ§€λ₯Ό 받을 λ•ŒκΉŒμ§€ λŒ€κΈ°ν•©λ‹ˆλ‹€.

//
// from pgconn/auth_oauth.go in github.com/xugy99/pgx
//
func (c *PgConn) rxOAuthSASLOk() (*pgproto3.AuthenticationOk, error) {
   // Block until we receive a message from the PostgreSQL server
   msg, err := c.receiveMessage()
   if err != nil {
       return nil, err
   }
   switch m := msg.(type) {
   case *pgproto3.AuthenticationOk:
       return m, nil
   case *pgproto3.ErrorResponse:
       return nil, ErrorResponseToPgError(m)
   // AuthenticationSASLContinue is received in when the token
   // is empty or invalid in SASLInitialResponse phase
   //case *pgproto3.AuthenticationSASLContinue:
   //      return nil, fmt.Errorf(": %s", string(m.Data))
   }
   return nil, fmt.Errorf("expected AuthenticationOk message but received unexpected %T", msg)
}

이 μ˜ˆμ œμ—μ„œλŠ” SASLInitialResponse에 항상 μœ νš¨ν•œ 토큰을 μ „λ‹¬ν•œλ‹€κ³  κ°€μ •ν•˜λ―€λ‘œ AuthenticationSASLContinue 응닡을 μ²˜λ¦¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ‹€λ§Œ μ‹€μ œ κ΅¬ν˜„μ—μ„œλŠ” AuthenticationSASLContinue μ²˜λ¦¬λ„ κ³ λ €ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ»€μŠ€ν…€ 검증기가 μ „λ‹¬λœ 토큰을 κ²€μ¦ν•˜μ§€ λͺ»ν•˜λ©΄ PostgreSQL이 OIDC λ””μŠ€μ»€λ²„λ¦¬ URL을 λ°˜ν™˜ν•΄ ν΄λΌμ΄μ–ΈνŠΈκ°€ μ§€μ •λœ IdPμ—μ„œ λ‹€μ‹œ μΈμ¦ν•˜λ„λ‘ μ•ˆλ‚΄ν•  수 있기 λ•Œλ¬Έμž…λ‹ˆλ‹€.

4. OAUTHBEARERλ₯Ό pgx μ—°κ²° λ‘œμ§μ— 톡합

OAUTHBEARER λ©”μ»€λ‹ˆμ¦˜μ˜ AuthenticationSASL을 μ²˜λ¦¬ν•˜λŠ” oauthAuth()λ₯Ό κ΅¬ν˜„ν–ˆλ‹€λ©΄ 이λ₯Ό pgconn/pgconn.go의 κΈ°λ³Έ μ—°κ²° 흐름에 톡합할 수 μžˆμŠ΅λ‹ˆλ‹€.

//
// from pgconn/pgconn.go in github.com/xugy99/pgx
//
func connectOne(ctx context.Context, config *Config, connectConfig *connectOneConfig,
    ignoreNotPreferredErr bool,
) (*PgConn, error) {
   // ... ...
   // ... ...
       case *pgproto3.AuthenticationSASL:
           // If the mechanism is OAUTHBEARER
           if slices.Contains(msg.AuthMechanisms, "OAUTHBEARER") {
               err = pgConn.oauthAuth()
               if err != nil {
                   pgConn.conn.Close()
                   return nil, newPerDialConnectError("failed OAUTHBEARER auth", err)
               }
               continue
           }
           // Fall back to SCRAM authentication
           err = pgConn.scramAuth(msg.AuthMechanisms)
           // ... error handling
   // ... ...

   // ... ...
}

5. OAuth Bearer 토큰을 μœ„ν•œ Config ν™•μž₯

이 μ˜ˆμ œμ—μ„œλŠ” pgconn의 configκ°€ 이미 λ°œκΈ‰λœ Bearer 토큰을 λ³΄μœ ν•˜κ³  μžˆλ‹€κ³  κ°€μ •ν•©λ‹ˆλ‹€. 이λ₯Ό μœ„ν•΄ pgconn/config.go에 ν•„μš”ν•œ 섀정을 μΆ”κ°€ν•©λ‹ˆλ‹€.

//
// from pgconn/config.go in github.com/xugy99/pgx
//
// Config is the settings used to establish a connection to a PostgreSQL server. It must be created by [ParseConfig]. A
// manually initialized Config will cause ConnectConfig to panic.
type Config struct {
   // ... existing fields ...
   // The preconfigured OAuth2 Bearer Token for authentication
   OAuthBearerToken string
}

// ParseConfigOptions extension
type ParseConfigOptions struct {
   // ... existing fields ...
   // The callback function to get the OAuth bear token
   GetOAuthBearerToken GetOAuthBearerTokenFunc
}

type GetOAuthBearerTokenFunc func(ctx context.Context) string

GetOAuthBearerTokenFunc()λŠ” pgx.ConnectWithOptions() μ˜΅μ…˜μ„ 톡해 OAuth 2.0 토큰을 κ°€μ Έμ˜€λŠ” μ½œλ°±μž…λ‹ˆλ‹€. λ‹€μŒ ν…ŒμŠ€νŠΈ ν”„λ‘œκ·Έλž¨μ—μ„œλŠ” ν™˜κ²½ λ³€μˆ˜μ—μ„œ 토큰을 κ°€μ Έμ˜€λŠ” λ‹¨μˆœν•œ λ‘œμ§μ„ μ‚¬μš©ν•©λ‹ˆλ‹€.


OAUTHBEARER 인증 ν΄λΌμ΄μ–ΈνŠΈ μ‹€ν–‰ 및 ν…ŒμŠ€νŠΈ

OAuth Bearer 토큰 κ°€μ Έμ˜€κΈ°

GetOAuthBearerTokenFuncλ₯Ό ν΄λ‘œμ €λ‘œ μ‚¬μš©ν•˜λ©΄ λ‹€μŒκ³Ό 같이 OAuth 인증에 ν•„μš”ν•œ 토큰을 κ°€μ Έμ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

func main() {
   // ... ...
   // ... ...
   opts.GetOAuthBearerToken = func(ctx context.Context) string {
       if et := os.Getenv("OAUTH_BEARER_TOKEN"); et != "" {
           return et
       }
       log.Println("using default fake token, you should expect to see login failure")
       return "whatever_token"
   }
   conn, err := pgx.ConnectWithOptions(context.Background(), *url, opts)
   // handling error ... ...
   // ... ...
}

OAuth2 ν΄λΌμ΄μ–ΈνŠΈ 전체 κ΅¬ν˜„ 예제

λ‹€μŒμ€ ν™•μž₯ν•œ pgx 라이브러리둜 OAuth 인증을 μˆ˜ν–‰ν•˜κ³  쿼리 κ²°κ³Όλ₯Ό ν…Œμ΄λΈ” ν˜•νƒœλ‘œ 좜λ ₯ν•˜λŠ” 전체 μ˜ˆμ œμž…λ‹ˆλ‹€.

//
// from examples/oauthtest/main.go in github.com/xugy99/pgx
//
package main
import (
   "context"
   "flag"
   "fmt"
   "log"
   "os"
   "github.com/jackc/pgx/v5"
   "github.com/pterm/pterm"
)
var (
   sql = flag.String("sql", "select version()", "SQL query to execute")
   url = flag.String("url", "postgres://joe@localhost:5432/my_db", "Database URL")
)
func main() {
   var (
       err  error
       opts pgx.ParseConfigOptions
   )
   flag.Parse()
   if *url == "" {
       log.Fatal("missing url")
   }
   // connect to server
   opts.GetOAuthBearerToken = func(ctx context.Context) string {
       if et := os.Getenv("OAUTH_BEARER_TOKEN"); et != "" {
           return et
       }
       log.Println("using default fake token, you should expect to see login failure")
       return "whatever_token"
   }
   conn, err := pgx.ConnectWithOptions(context.Background(), *url, opts)
   if err != nil {
       log.Fatalf("Unable to connect PostgreSQL by OAUTHBEARER: %v\n", err)
   }
   defer conn.Close(context.Background())
   // run sql
   rows, err := conn.Query(context.Background(), *sql)
   if err != nil {
       log.Fatalf("Failed to execute SQL: %v\n", err)
   }
   defer rows.Close()
   // convert to pterm table and print the result
   data := pterm.TableData{{}}
   //   - header row
   fieldDescriptions := rows.FieldDescriptions()
   for _, fd := range fieldDescriptions {
       data[0] = append(data[0], string(fd.Name))
   }
   //   - data row
   for rows.Next() {
       if rows.Err() == pgx.ErrNoRows {
           break
       }
       var row []string
       columns, _ := rows.Values()
       for _, v := range columns {
           row = append(row, fmt.Sprintf("%v", v))
       }
       data = append(data, row)
   }
   pterm.DefaultTable.
       WithBoxed(true).
       WithHasHeader(true).
       WithHeaderRowSeparator("-").
       WithData(data).
       Render() // show it
}

OAUTHBEARER 인증 ν…ŒμŠ€νŠΈ

이 μ‹œλ¦¬μ¦ˆ Part-2μ—μ„œ μ„€μ •ν•œ PostgreSQL 18 RC1 μ„œλ²„κ°€ μžˆλ‹€λ©΄ λ¨Όμ € psql을 μ‚¬μš©ν•΄ νŠΈλ ˆμ΄μŠ€μ—μ„œ 토큰을 κ°€μ Έμ˜΅λ‹ˆλ‹€.

PGOAUTHDEBUG=UNSAFE psql "postgres://joe@localhost:5432/my_db?oauth_issuer=http://your-oauth-server/auth&oauth_client_id=your-client-id" -c "SELECT current_user, session_user, version()"

좜λ ₯λ˜λŠ” νŠΈλ ˆμ΄μŠ€μ—λŠ” access_token이 ν¬ν•¨λ˜λ©° μ˜ˆμ‹œλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

[libcurl] <   "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImQ2YThlYTIxZjllN2QzYmJhNTA0NWNhZDBhN2ExZmY1MGM1ZTRkMzgifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjU1NTYvYXV0aCIsInN1YiI6IkNpUmtOakkzTmpnME9TMDRNR1prTFRSak5Ea3RZak5oTkMwd1pUWTFObUpsTVRVeVpUQVNCV3h2WTJGcyIsImF1ZCI6InlvdXItY2xpZW50LWlkIiwiZXhwIjoxNzU4MzczMzMwLCJpYXQiOjE3NTgyODY5MzAsImF0X2hhc2giOiJDLUsyTUlwaG42VGUxUE5lMzlEXy1RIiwibmFtZSI6IkpvZSBEb2UifQ.LJXaixCj9tLNeY_kLxgPH0kn497dEtoHpLswLBVBUh5z7rE46Ixt72xxeHVxHke32fjjnsE3Zh_rR4pcUspkfl7a_L4ElZZaI2AK6e_4YX5x3GHN4yazxgBS7a5U8cUYt5s6Gizox6199Mm50jg5Ho9AHoldkV2JxE6foaSHrp2EZr6zM4ZWPeW_EBC0GVB4ngjaYsaEjAqNLUVePviqv6H70omM5GF6JyFxSGYTpbpUuji8S3CmTCFKJaDNjgk2R5BFrLzLhJaXyPjdcBpDLcZKrKc4NtVCSptAVOLJto5qr1ICeLGanGgg4q-BA-EtbKSd53Dxac_Uu4lmrPJycg",<0A>
[libcurl] <   "token_type": "bearer",<0A>
[libcurl] <   "expires_in": 86399,<0A>

 

λ‹€μŒ λ‹¨κ³„μ—μ„œ μ‚¬μš©ν•  access_token을 ν™˜κ²½ λ³€μˆ˜μ— μ €μž₯ν•©λ‹ˆλ‹€.

export OAUTH_BEARER_TOKEN=<access_token>

이후 예제 ν”„λ‘œκ·Έλž¨μ„ μ‹€ν–‰ν•©λ‹ˆλ‹€.

cd examples/oauthtest
go run main.go -url 'postgres://joe@localhost:5432/my_db' -sql 'select current_user,session_user,version()'

μ •μƒμ μœΌλ‘œ 인증되면 λ‹€μŒκ³Ό 같은 κ²°κ³Όλ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
| current_user | session_user | version                                                                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------------------ |
| joe          | joe          | PostgreSQL 18rc1 on aarch64-apple-darwin24.6.0, compiled by Apple clang version 17.0.0 (clang-1700.0.13.5), 64-bit |
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

독립적인 토큰 쑰회 및 κ°±μ‹  λ©”μ»€λ‹ˆμ¦˜μ€ pgxpool의 config.BeforeConnect() μ½œλ°±μ— κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 ν’€μ—μ„œ μƒμ„±λ˜λŠ” μƒˆ 연결이 μ΅œμ‹  ν† ν°μœΌλ‘œ μΈμ¦λ˜λ„λ‘ ꡬ성할 수 μžˆμŠ΅λ‹ˆλ‹€.


OAUTHBEARER κ΅¬ν˜„ μ‹œ μΆ”κ°€λ‘œ κ³ λ €ν•  κΈ°λŠ₯

AuthenticationSASLContinue 응닡 처리

이 κΈ€μ˜ pgx μ˜ˆμ œμ—λŠ” PostgreSQL μ„œλ²„κ°€ λ°˜ν™˜ν•˜λŠ” OIDC λ””μŠ€μ»€λ²„λ¦¬μ— λŒ€ν•œ AuthenticationSASLContinue μ²˜λ¦¬κ°€ κ΅¬ν˜„λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€. ν΄λΌμ΄μ–ΈνŠΈκ°€ 이 λ©”μ‹œμ§€μ— μ–΄λ–»κ²Œ λŒ€μ‘ν• μ§€λŠ” μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 인증 ꡬ쑰에 따라 λ‹¬λΌμ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

  • 토큰이 λ§Œλ£Œλ˜μ—ˆκ±°λ‚˜ μœ νš¨ν•˜μ§€ μ•Šμ€ 경우 OAuth2 인가 흐름을 λ³„λ„μ˜ μ»΄ν¬λ„ŒνŠΈλ‚˜ μ‹œμŠ€ν…œμ—μ„œ μ²˜λ¦¬ν•˜λ„λ‘ κ΅¬μ„±ν–ˆλ‹€λ©΄ 였λ₯˜λ₯Ό λ°˜ν™˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • λ²”μš© PostgreSQL ν΄λΌμ΄μ–ΈνŠΈ ν˜•νƒœλ‘œ κ΅¬ν˜„ν•˜λ €λ©΄ μ„œλ²„κ°€ μ§€μ •ν•œ IdPλ₯Ό ν†΅ν•œ 인가 흐름을 μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

pgx μ—°κ²° ν’€μ—μ„œ OAuth2 토큰 관리

사전에 κ΅¬μ„±ν•œ 토큰은 pgx μ—°κ²° 풀에도 톡합할 수 μžˆμŠ΅λ‹ˆλ‹€. λ³Έλ¬Έμ—μ„œ μ°Έμ‘°ν•œ https://github.com/xugy99/pgx μ½”λ“œμ—λŠ” 이 뢀뢄이 ν¬ν•¨λ˜μ–΄ μžˆμ§€ μ•Šμ§€λ§Œ λ‹€μŒκ³Ό 같은 λ°©μ‹μœΌλ‘œ ꡬ성할 수 μžˆμŠ΅λ‹ˆλ‹€.

config, err := pgxpool.ParseConfig("postgres://joe@localhost:5432/my_db")
if err != nil {
   return err
}
config.ConnConfig.OAuthBearerToken = "my_token"
pool, err := pgxpool.NewWithConfig(context.Background(), config)
if err != nil {
   return err
}
defer pool.Close()

pgx의 λ„€μ΄ν‹°λΈŒ OAuth2 인가 흐름 κ΅¬ν˜„

Go μƒνƒœκ³„μ—λŠ” OAuth2 인가 흐름을 κ΅¬ν˜„ν•  수 μžˆλŠ” λ‹€μ–‘ν•œ λΌμ΄λΈŒλŸ¬λ¦¬μ™€ 방식이 μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό pgx의 μ—°κ²° 및 인증 ꡬ쑰와 κ²°ν•©ν•˜λ©΄ PostgreSQL ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ OAuth2 인가 흐름을 직접 μ²˜λ¦¬ν•˜λŠ” ꡬ쑰둜 ν™•μž₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€.


PostgreSQL ν΄λΌμ΄μ–ΈνŠΈμ˜ OAuth2 인증 ν™•μž₯

이번 pgx κ΅¬ν˜„ 예제λ₯Ό 톡해 PostgreSQL ν΄λΌμ΄μ–ΈνŠΈ λΌμ΄λΈŒλŸ¬λ¦¬μ— OAUTHBEARER 인증을 μΆ”κ°€ν•˜κΈ° μœ„ν•΄ ν•„μš”ν•œ μ£Όμš” ꡬ쑰λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

  • ꡬ성 ν™•μž₯: OAuth 토큰을 ν΄λΌμ΄μ–ΈνŠΈ μ—°κ²° 섀정에 전달
  • SASL 인증 ν”„λ ˆμž„μ›Œν¬ 톡합: κΈ°μ‘΄ 인증 처리 ꡬ쑰에 OAUTHBEARER 흐름 μΆ”κ°€
  • RFC 7628: OAuth Bearer 토큰 인증에 ν•„μš”ν•œ λ©”μ‹œμ§€ ν˜•μ‹ 적용

PostgreSQL 18의 OAuth2 인증은 ν΄λΌμš°λ“œ λ„€μ΄ν‹°λΈŒ ν™˜κ²½μ—μ„œ OAuth 2.0 기반 인증을 ν™œμš©ν•˜λŠ” PostgreSQL ν΄λΌμ΄μ–ΈνŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ κ΅¬ν˜„ν•  수 μžˆλŠ” κΈ°λ°˜μ„ μ œκ³΅ν•©λ‹ˆλ‹€.


PostgreSQL 18 OAuth2 개발 참고 자료

 

메일: salesinquiry@enterprisedb.com

PostgreSQL λ„μž…λΆ€ν„° μš΄μ˜κΉŒμ§€, EDB와 ν•¨κ»˜ν•˜μ„Έμš”

PostgreSQL λ„μž…, 운영, μ„±λŠ₯ μ΅œμ ν™” 및 μ—…κ·Έλ ˆμ΄λ“œμ— λŒ€ν•΄ EDB 전문가와 상담해 λ³΄μ„Έμš”.

λ¬Έμ˜ν•˜κΈ°

 

Share this