Skip to content

Commit 2d76f50

Browse files
jkatzelprans
authored andcommitted
Add support for SCRAM-SHA-256 authentication.
SCRAM-SHA-256 authentication was introduced in PostgreSQL 10 as a better way for handling password based authentication. This implementation follows the guidance provided in the documentation, i.e. https://www.postgresql.org/docs/current/sasl-authentication.html#SASL-SCRAM-SHA-256
1 parent 92c2d81 commit 2d76f50

File tree

5 files changed

+551
-6
lines changed

5 files changed

+551
-6
lines changed

asyncpg/protocol/coreproto.pxd

+11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
# the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0
66

77

8+
include "scram.pxd"
9+
10+
811
cdef enum ConnectionStatus:
912
CONNECTION_OK = 1
1013
CONNECTION_BAD = 2
@@ -43,13 +46,17 @@ cdef enum AuthenticationMessage:
4346
AUTH_REQUIRED_GSS = 7
4447
AUTH_REQUIRED_GSS_CONTINUE = 8
4548
AUTH_REQUIRED_SSPI = 9
49+
AUTH_REQUIRED_SASL = 10
50+
AUTH_SASL_CONTINUE = 11
51+
AUTH_SASL_FINAL = 12
4652

4753

4854
AUTH_METHOD_NAME = {
4955
AUTH_REQUIRED_KERBEROS: 'kerberosv5',
5056
AUTH_REQUIRED_PASSWORD: 'password',
5157
AUTH_REQUIRED_PASSWORDMD5: 'md5',
5258
AUTH_REQUIRED_GSS: 'gss',
59+
AUTH_REQUIRED_SASL: 'scram-sha-256',
5360
AUTH_REQUIRED_SSPI: 'sspi',
5461
}
5562

@@ -91,6 +98,8 @@ cdef class CoreProtocol:
9198

9299
# Instance of _ConnectionParameters
93100
object con_params
101+
# Instance of SCRAMAuthentication
102+
SCRAMAuthentication scram
94103

95104
readonly int32_t backend_pid
96105
readonly int32_t backend_secret
@@ -133,6 +142,8 @@ cdef class CoreProtocol:
133142

134143
cdef _auth_password_message_cleartext(self)
135144
cdef _auth_password_message_md5(self, bytes salt)
145+
cdef _auth_password_message_sasl_initial(self, list sasl_auth_methods)
146+
cdef _auth_password_message_sasl_continue(self, bytes server_response)
136147

137148
cdef _write(self, buf)
138149

asyncpg/protocol/coreproto.pyx

+89-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from hashlib import md5 as hashlib_md5 # for MD5 authentication
99

1010

11+
include "scram.pyx"
12+
13+
1114
cdef class CoreProtocol:
1215

1316
def __init__(self, con_params):
@@ -21,6 +24,8 @@ cdef class CoreProtocol:
2124
self.state = PROTOCOL_IDLE
2225
self.xact_status = PQTRANS_IDLE
2326
self.encoding = 'utf-8'
27+
# type of `scram` is `SCRAMAuthentcation`
28+
self.scram = None
2429

2530
# executemany support data
2631
self._execute_iter = None
@@ -528,6 +533,8 @@ cdef class CoreProtocol:
528533
cdef:
529534
int32_t status
530535
bytes md5_salt
536+
list sasl_auth_methods
537+
list unsupported_sasl_auth_methods
531538

532539
status = self.buffer.read_int32()
533540

@@ -546,6 +553,58 @@ cdef class CoreProtocol:
546553
md5_salt = self.buffer.read_bytes(4)
547554
self.auth_msg = self._auth_password_message_md5(md5_salt)
548555

556+
elif status == AUTH_REQUIRED_SASL:
557+
# AuthenticationSASL
558+
# This requires making additional requests to the server in order
559+
# to follow the SCRAM protocol defined in RFC 5802.
560+
# get the SASL authentication methods that the server is providing
561+
sasl_auth_methods = []
562+
unsupported_sasl_auth_methods = []
563+
# determine if the advertised authentication methods are supported,
564+
# and if so, add them to the list
565+
auth_method = self.buffer.read_null_str()
566+
while auth_method:
567+
if auth_method in SCRAMAuthentication.AUTHENTICATION_METHODS:
568+
sasl_auth_methods.append(auth_method)
569+
else:
570+
unsupported_sasl_auth_methods.append(auth_method)
571+
auth_method = self.buffer.read_null_str()
572+
573+
# if none of the advertised authentication methods are supported,
574+
# raise an error
575+
# otherwise, initialize the SASL authentication exchange
576+
if not sasl_auth_methods:
577+
unsupported_sasl_auth_methods = [m.decode("ascii")
578+
for m in unsupported_sasl_auth_methods]
579+
self.result_type = RESULT_FAILED
580+
self.result = apg_exc.InterfaceError(
581+
'unsupported SASL Authentication methods requested by the '
582+
'server: {!r}'.format(
583+
", ".join(unsupported_sasl_auth_methods)))
584+
else:
585+
self.auth_msg = self._auth_password_message_sasl_initial(
586+
sasl_auth_methods)
587+
588+
elif status == AUTH_SASL_CONTINUE:
589+
# AUTH_SASL_CONTINUE
590+
# this requeires sending the second part of the SASL exchange, where
591+
# the client parses information back from the server and determines
592+
# if this is valid.
593+
# The client builds a challenge response to the server
594+
server_response = self.buffer.consume_message()
595+
self.auth_msg = self._auth_password_message_sasl_continue(
596+
server_response)
597+
598+
elif status == AUTH_SASL_FINAL:
599+
# AUTH_SASL_FINAL
600+
server_response = self.buffer.consume_message()
601+
if not self.scram.verify_server_final_message(server_response):
602+
self.result_type = RESULT_FAILED
603+
self.result = apg_exc.InterfaceError(
604+
'could not verify server signature for '
605+
'SCRAM authentciation: scram-sha-256',
606+
)
607+
549608
elif status in (AUTH_REQUIRED_KERBEROS, AUTH_REQUIRED_SCMCRED,
550609
AUTH_REQUIRED_GSS, AUTH_REQUIRED_GSS_CONTINUE,
551610
AUTH_REQUIRED_SSPI):
@@ -560,7 +619,8 @@ cdef class CoreProtocol:
560619
'unsupported authentication method requested by the '
561620
'server: {}'.format(status))
562621

563-
self.buffer.discard_message()
622+
if status not in [AUTH_SASL_CONTINUE, AUTH_SASL_FINAL]:
623+
self.buffer.discard_message()
564624

565625
cdef _auth_password_message_cleartext(self):
566626
cdef:
@@ -588,6 +648,34 @@ cdef class CoreProtocol:
588648

589649
return msg
590650

651+
cdef _auth_password_message_sasl_initial(self, list sasl_auth_methods):
652+
cdef:
653+
WriteBuffer msg
654+
655+
# use the first supported advertized mechanism
656+
self.scram = SCRAMAuthentication(sasl_auth_methods[0])
657+
# this involves a call and response with the server
658+
msg = WriteBuffer.new_message(b'p')
659+
msg.write_bytes(self.scram.create_client_first_message(self.user or ''))
660+
msg.end_message()
661+
662+
return msg
663+
664+
cdef _auth_password_message_sasl_continue(self, bytes server_response):
665+
cdef:
666+
WriteBuffer msg
667+
668+
# determine if there is a valid server response
669+
self.scram.parse_server_first_message(server_response)
670+
# this involves a call and response with the server
671+
msg = WriteBuffer.new_message(b'p')
672+
client_final_message = self.scram.create_client_final_message(
673+
self.password or '')
674+
msg.write_bytes(client_final_message)
675+
msg.end_message()
676+
677+
return msg
678+
591679
cdef _parse_msg_ready_for_query(self):
592680
cdef char status = self.buffer.read_byte()
593681

asyncpg/protocol/scram.pxd

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright (C) 2016-present the asyncpg authors and contributors
2+
# <see AUTHORS file>
3+
#
4+
# This module is part of asyncpg and is released under
5+
# the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0
6+
7+
8+
cdef class SCRAMAuthentication:
9+
cdef:
10+
readonly bytes authentication_method
11+
readonly bytes authorization_message
12+
readonly bytes client_channel_binding
13+
readonly bytes client_first_message_bare
14+
readonly bytes client_nonce
15+
readonly bytes client_proof
16+
readonly bytes password_salt
17+
readonly int password_iterations
18+
readonly bytes server_first_message
19+
# server_key is an instance of hmac.HAMC
20+
readonly object server_key
21+
readonly bytes server_nonce
22+
23+
cdef create_client_first_message(self, str username)
24+
cdef create_client_final_message(self, str password)
25+
cdef parse_server_first_message(self, bytes server_response)
26+
cdef verify_server_final_message(self, bytes server_final_message)
27+
cdef _bytes_xor(self, bytes a, bytes b)
28+
cdef _generate_client_nonce(self, int num_bytes)
29+
cdef _generate_client_proof(self, str password)
30+
cdef _generate_salted_password(self, str password, bytes salt, int iterations)
31+
cdef _normalize_password(self, str original_password)

0 commit comments

Comments
 (0)