PostgreSQL 18 OAuth2 OAUTHBEARER ν΄λΌμ΄μΈνΈ ꡬν λ°©λ² (3)
μμ±μ: EDB Korea Team
2025λ
9μ 19μΌ
PostgreSQL 18 OAuth2 ν΄λΌμ΄μΈνΈ ꡬν λ°©μ
PostgreSQL 18μ OAuth2 μΈμ¦μ λμ ν΄ κΈ°μ‘΄ μ¬μ©μλͺ κ³Ό λΉλ°λ²νΈ μ‘°ν© λμ OAuth2 ν ν°μΌλ‘ μ¬μ©μλ₯Ό μΈμ¦ν μ μλλ‘ μ§μν©λλ€. μ΄ κ°μ΄λμμλ λ리 μ¬μ©λλ pgx Go λλΌμ΄λ²λ₯Ό μ°Έμ‘° ꡬνμΌλ‘ νμ©ν΄ PostgreSQL ν΄λΌμ΄μΈνΈ λΌμ΄λΈλ¬λ¦¬μ OAuth Bearer ν ν° μΈμ¦μ ꡬννλ λ°©λ²μ μ΄ν΄λ΄ λλ€.
μ΄ μ리μ¦λ μ΄ 3νΈμΌλ‘ ꡬμ±λ©λλ€.
- Part-1: PostgreSQL 18 OAuth2 μΈμ¦ μλ μ리μ μ€μ λ°©λ²
- Part-2: PostgreSQL 18 OAuth2 컀μ€ν κ²μ¦κΈ° ꡬν λ°©λ²
- Part-3 (μ΄ κΈ): PostgreSQL 18 OAuth2 OAUTHBEARER ν΄λΌμ΄μΈνΈ ꡬν λ°©λ²
μ΄ κΈμμ μ¬μ©νλ μ½λλ νμμ 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 μΈμ¦ νλ¦μ μ²λ¦¬ν©λλ€.
SASLInitialResponseAuthenticationSASLContinueSASLResponseAuthenticationSASLFinal
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 ReadyC: t0 CAPABILITYS: * CAPABILITY IMAP4rev1 AUTH=OAUTHBEARER SASL-IRS: t0 OK CompletedC: t1 AUTHENTICATE OAUTHBEARER bixhPXVzZXJAZXhhbXBsZS5jb20sAWhvc3Q9c2VydmVyLmV4YW1wbGUuY29tAXBvcnQ9MTQzAWF1dGg9QmVhcmVyIHZGOWRmdDRxbVRjMk52YjNSbGNrQmhiSFJoZG1semRHRXVZMjl0Q2c9PQEBS: t1 OK SASL authentication succeededAs required by IMAP [RFC3501], the payloads are base64 encoded. Thedecoded initial client response (with %x01 represented as ^A and longlines wrapped for readability) is:n,a=user@example.com,^Ahost=server.example.com^Aport=143^Aauth=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 κ°λ° μ°Έκ³ μλ£
- RFC 7628: A Set of SASL Mechanisms for OAuth
- PostgreSQL 18 λ¬Έμ: Authentication
- pgx Go Driver
- PostgreSQL Wire Protocol
- OAuth 2.0 Bearer Token Usage
λ©μΌ: salesinquiry@enterprisedb.com
PostgreSQL λμ λΆν° μ΄μκΉμ§, EDBμ ν¨κ»νμΈμ
PostgreSQL λμ , μ΄μ, μ±λ₯ μ΅μ ν λ° μ κ·Έλ μ΄λμ λν΄ EDB μ λ¬Έκ°μ μλ΄ν΄ 보μΈμ.

