Skip to content

Commit b7a9f54

Browse files
committed
Merge branch 'jul/oidc-auth-code-verification' into 'master'
Add authorization code verification method See merge request TankerHQ/sdk-python!304
2 parents ed67edd + 0c14aea commit b7a9f54

5 files changed

Lines changed: 190 additions & 25 deletions

File tree

cffi_defs.h

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -61,29 +61,10 @@ struct tanker_error
6161
char const* message;
6262
};
6363

64-
// ctanker/network.h
65-
66-
struct tanker_http_request
67-
{
68-
char const* method;
69-
char const* url;
70-
char const* instance_id;
71-
char const* authorization;
72-
char const* body;
73-
int32_t body_size;
74-
};
75-
76-
struct tanker_http_response
77-
{
78-
char const* error_msg;
79-
char const* content_type;
80-
char const* body;
81-
int64_t body_size;
82-
int32_t status_code;
83-
};
64+
// ctanker/http.h There is no HTTP revese bindings for python
8465

66+
// foward declarations to satisfy FFI
8567
typedef struct tanker_http_request tanker_http_request_t;
86-
typedef struct tanker_http_response tanker_http_response_t;
8768

8869
typedef void tanker_http_request_handle_t;
8970

@@ -103,9 +84,6 @@ struct tanker_http_options
10384

10485
typedef struct tanker_http_options tanker_http_options_t;
10586

106-
void tanker_http_handle_response(tanker_http_request_t*,
107-
tanker_http_response_t*);
108-
10987
// ctanker/datastore.h
11088

11189
enum tanker_datastore_error_code
@@ -216,6 +194,7 @@ enum tanker_verification_method_type
216194
TANKER_VERIFICATION_METHOD_PREVERIFIED_PHONE_NUMBER,
217195
TANKER_VERIFICATION_METHOD_E2E_PASSPHRASE,
218196
TANKER_VERIFICATION_METHOD_PREVERIFIED_OIDC,
197+
TANKER_VERIFICATION_METHOD_OIDC_AUTHORIZATION_CODE,
219198

220199
TANKER_VERIFICATION_METHOD_LAST
221200
};
@@ -233,6 +212,7 @@ typedef struct tanker_options tanker_options_t;
233212
typedef struct tanker_email_verification tanker_email_verification_t;
234213
typedef struct tanker_phone_number_verification tanker_phone_number_verification_t;
235214
typedef struct tanker_preverified_oidc_verification tanker_preverified_oidc_verification_t;
215+
typedef struct tanker_oidc_authorization_code_verification tanker_oidc_authorization_code_verification_t;
236216
typedef struct tanker_verification tanker_verification_t;
237217
typedef struct tanker_verification_list tanker_verification_list_t;
238218
typedef struct tanker_verification_method tanker_verification_method_t;
@@ -306,6 +286,14 @@ struct tanker_preverified_oidc_verification
306286
char const* provider_id;
307287
};
308288

289+
struct tanker_oidc_authorization_code_verification
290+
{
291+
uint8_t version;
292+
char const* provider_id;
293+
char const* authorization_code;
294+
char const* state;
295+
};
296+
309297
struct tanker_verification
310298
{
311299
uint8_t version;
@@ -321,6 +309,7 @@ struct tanker_verification
321309
char const* preverified_email;
322310
char const* preverified_phone_number;
323311
tanker_preverified_oidc_verification_t preverified_oidc_verification;
312+
tanker_oidc_authorization_code_verification_t oidc_authorization_code_verification;
324313
};
325314

326315
struct tanker_verification_method
@@ -442,6 +431,9 @@ tanker_future_t* tanker_attach_provisional_identity(
442431
tanker_future_t* tanker_verify_provisional_identity(
443432
tanker_t* ctanker, tanker_verification_t const* verification);
444433

434+
tanker_expected_t* tanker_authenticate_with_idp(tanker_t* session, char const* provider_id, char const* cookie);
435+
void tanker_free_authenticate_with_idp_result(tanker_oidc_authorization_code_verification_t* result);
436+
445437
void tanker_free_buffer(void const* buffer);
446438

447439
void tanker_free_verification_method_list(

tankersdk/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
EmailVerification,
1717
EmailVerificationMethod,
1818
EncryptionOptions,
19+
OidcAuthorizationCodeVerification,
1920
OidcIdTokenVerification,
2021
OidcIdTokenVerificationMethod,
2122
Padding,

tankersdk/experimental.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from _tanker import lib as tankerlib
2+
3+
from .tanker import OidcAuthorizationCodeVerification, Tanker, ffihelpers
4+
5+
6+
async def authenticate_with_idp(
7+
tanker: Tanker, provider_id: str, cookie: str
8+
) -> OidcAuthorizationCodeVerification:
9+
c_provider_id = ffihelpers.str_to_c_string(provider_id)
10+
c_cookie = ffihelpers.str_to_c_string(cookie)
11+
c_expected_verification = tankerlib.tanker_authenticate_with_idp(
12+
tanker.c_tanker, c_provider_id, c_cookie
13+
)
14+
15+
c_verification = ffihelpers.unwrap_expected(
16+
c_expected_verification, "tanker_oidc_authorization_code_verification_t*"
17+
)
18+
c_authorization_code = c_verification.authorization_code
19+
c_state = c_verification.state
20+
21+
authorization_code = ffihelpers.c_string_to_str(c_authorization_code)
22+
state = ffihelpers.c_string_to_str(c_state)
23+
24+
tankerlib.tanker_free_authenticate_with_idp_result(c_verification)
25+
return OidcAuthorizationCodeVerification(provider_id, authorization_code, state)

tankersdk/tanker.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class VerificationMethodType(Enum):
6363
PREVERIFIED_PHONE_NUMBER = 7
6464
E2E_PASSPHRASE = 8
6565
PREVERIFIED_OIDC = 9
66+
OIDC_AUTHORIZATION_CODE = 10
6667

6768

6869
class Verification:
@@ -148,6 +149,15 @@ def __init__(self, subject: str, provider_id: str):
148149
self.provider_id = provider_id
149150

150151

152+
class OidcAuthorizationCodeVerification(Verification):
153+
method_type = VerificationMethodType.OIDC_AUTHORIZATION_CODE
154+
155+
def __init__(self, provider_id: str, authorization_code: str, state: str):
156+
self.provider_id = provider_id
157+
self.authorization_code = authorization_code
158+
self.state = state
159+
160+
151161
class VerificationMethod:
152162
# Note: we want every subclass to have a 'mehod_type' attribute
153163
# of type VerificationMethodType, but there's no "good"
@@ -390,7 +400,7 @@ def __init__(
390400

391401
# Note: we store things in `self` so they don't get
392402
# garbage collected later on
393-
c_verification = ffi.new("tanker_verification_t *", {"version": 7})
403+
c_verification = ffi.new("tanker_verification_t *", {"version": 8})
394404
if isinstance(verification, VerificationKeyVerification):
395405
c_verification.verification_method_type = (
396406
tankerlib.TANKER_VERIFICATION_METHOD_VERIFICATION_KEY
@@ -479,6 +489,21 @@ def __init__(
479489
c_verification.preverified_oidc_verification = (
480490
self._preverified_oidc_verification
481491
)
492+
elif isinstance(verification, OidcAuthorizationCodeVerification):
493+
c_verification.verification_method_type = (
494+
tankerlib.TANKER_VERIFICATION_METHOD_OIDC_AUTHORIZATION_CODE
495+
)
496+
self._oidc_authorization_code_verification = {
497+
"version": 1,
498+
"provider_id": ffihelpers.str_to_c_string(verification.provider_id),
499+
"authorization_code": ffihelpers.str_to_c_string(
500+
verification.authorization_code
501+
),
502+
"state": ffihelpers.str_to_c_string(verification.state),
503+
}
504+
c_verification.oidc_authorization_code_verification = (
505+
self._oidc_authorization_code_verification
506+
)
482507

483508
self._c_verification = c_verification
484509

test/test_tanker.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
VerificationOptions,
4242
error,
4343
)
44+
from tankersdk.experimental import authenticate_with_idp
4445

4546

4647
def encode(string: str) -> str:
@@ -68,6 +69,7 @@ def read_test_config() -> Dict[str, Any]:
6869
"clientSecret": assert_env("TANKER_OIDC_CLIENT_SECRET"),
6970
"provider": assert_env("TANKER_OIDC_PROVIDER"),
7071
"issuer": assert_env("TANKER_OIDC_ISSUER"),
72+
"fakeOidcIssuerUrl": assert_env("TANKER_FAKE_OIDC_URL") + "/issuer",
7173
}
7274
res["oidc"]["users"] = {
7375
"martine": {
@@ -1546,6 +1548,24 @@ def set_up_oidc(app: Dict[str, str], admin: Admin) -> Dict[str, str]:
15461548
return cast(Dict[str, str], admin.get_app(app["id"])["oidc_providers"][0])
15471549

15481550

1551+
def set_up_fake_oidc(app: Dict[str, str], admin: Admin) -> Dict[str, str]:
1552+
fake_oidc_issuer_url = TEST_CONFIG["oidc"]["fakeOidcIssuerUrl"]
1553+
1554+
oidc_issuer = fake_oidc_issuer_url
1555+
oidc_client_id = "tanker"
1556+
oidc_provider = "fake-oidc"
1557+
admin.update_app(
1558+
app["id"],
1559+
oidc_providers=[
1560+
AppOidcProvider(
1561+
client_id=oidc_client_id, display_name=oidc_provider, issuer=oidc_issuer
1562+
)
1563+
],
1564+
)
1565+
1566+
return cast(Dict[str, str], admin.get_app(app["id"])["oidc_providers"][0])
1567+
1568+
15491569
@pytest.mark.asyncio
15501570
async def test_oidc_verification(
15511571
tmp_path: Path, app: Dict[str, str], admin: Admin
@@ -1584,6 +1604,61 @@ async def test_oidc_verification(
15841604
await martine_laptop.stop()
15851605

15861606

1607+
@pytest.mark.asyncio
1608+
async def test_oidc_authorization_code_verification(
1609+
tmp_path: Path, app: Dict[str, str], admin: Admin
1610+
) -> None:
1611+
provider_config = set_up_fake_oidc(app, admin)
1612+
provider_id = provider_config["id"]
1613+
subject_cookie = "fake_oidc_subject=martine"
1614+
1615+
phone_path = tmp_path / "phone"
1616+
phone_path.mkdir(exist_ok=True)
1617+
martine_phone = create_tanker(app["id"], persistent_path=phone_path)
1618+
identity = tankersdk_identity.create_identity(
1619+
app["id"], app["secret"], str(uuid.uuid4())
1620+
)
1621+
1622+
await martine_phone.start(identity)
1623+
1624+
verification1 = await authenticate_with_idp(
1625+
martine_phone, provider_id, subject_cookie
1626+
)
1627+
verification2 = await authenticate_with_idp(
1628+
martine_phone, provider_id, subject_cookie
1629+
)
1630+
await martine_phone.register_identity(verification1)
1631+
await martine_phone.stop()
1632+
1633+
laptop_path = tmp_path / "laptop"
1634+
laptop_path.mkdir(exist_ok=True)
1635+
martine_laptop = create_tanker(app["id"], persistent_path=laptop_path)
1636+
await martine_laptop.start(identity)
1637+
1638+
assert martine_laptop.status == TankerStatus.IDENTITY_VERIFICATION_NEEDED
1639+
await martine_laptop.verify_identity(verification2)
1640+
assert martine_laptop.status == TankerStatus.READY
1641+
1642+
actual_methods = await martine_laptop.get_verification_methods()
1643+
(actual_method,) = actual_methods
1644+
assert actual_method.method_type == VerificationMethodType.OIDC_ID_TOKEN
1645+
1646+
await martine_laptop.stop()
1647+
1648+
tablet_path = tmp_path / "tablet"
1649+
tablet_path.mkdir(exist_ok=True)
1650+
martine_tablet = create_tanker(app["id"], persistent_path=tablet_path)
1651+
await martine_tablet.start(identity)
1652+
verification3 = await authenticate_with_idp(
1653+
martine_tablet, provider_id, "fake_oidc_subject=not_martine"
1654+
)
1655+
1656+
assert martine_tablet.status == TankerStatus.IDENTITY_VERIFICATION_NEEDED
1657+
with pytest.raises(error.InvalidVerification):
1658+
await martine_tablet.verify_identity(verification3)
1659+
await martine_tablet.stop()
1660+
1661+
15871662
@pytest.mark.asyncio
15881663
async def test_register_fails_with_preverified_email(
15891664
tmp_path: Path, app: Dict[str, str], admin: Admin
@@ -1886,6 +1961,53 @@ async def test_set_verification_method_with_oidc(
18861961
await phone_tanker.stop()
18871962

18881963

1964+
@pytest.mark.asyncio
1965+
async def test_set_oidc_authorization_code_verification(
1966+
tmp_path: Path, app: Dict[str, str], admin: Admin
1967+
) -> None:
1968+
provider_config = set_up_fake_oidc(app, admin)
1969+
provider_id = provider_config["id"]
1970+
subject_cookie = "fake_oidc_subject=alice"
1971+
passphrase = "The cake is not a lie"
1972+
1973+
laptop_path = tmp_path / "laptop"
1974+
laptop_path.mkdir(exist_ok=True)
1975+
laptop_tanker = create_tanker(app["id"], persistent_path=laptop_path)
1976+
alice_identity = tankersdk_identity.create_identity(
1977+
app["id"], app["secret"], str(uuid.uuid4())
1978+
)
1979+
1980+
await laptop_tanker.start(alice_identity)
1981+
await laptop_tanker.register_identity(PassphraseVerification(passphrase))
1982+
1983+
verification2 = await authenticate_with_idp(
1984+
laptop_tanker, provider_id, subject_cookie
1985+
)
1986+
await laptop_tanker.set_verification_method(verification2)
1987+
1988+
methods = set(await laptop_tanker.get_verification_methods())
1989+
assert len(methods) == 2
1990+
oidc_methods = [x for x in methods if isinstance(x, OidcIdTokenVerificationMethod)]
1991+
assert oidc_methods[0].provider_id == provider_config["id"]
1992+
assert oidc_methods[0].provider_display_name == provider_config["display_name"]
1993+
1994+
phone_path = tmp_path.joinpath("phone")
1995+
phone_path.mkdir(exist_ok=True)
1996+
phone_tanker = create_tanker(app["id"], persistent_path=phone_path)
1997+
1998+
await phone_tanker.start(alice_identity)
1999+
assert phone_tanker.status == TankerStatus.IDENTITY_VERIFICATION_NEEDED
2000+
2001+
verification2 = await authenticate_with_idp(
2002+
laptop_tanker, provider_id, subject_cookie
2003+
)
2004+
await phone_tanker.verify_identity(verification2)
2005+
assert phone_tanker.status == TankerStatus.READY
2006+
2007+
await laptop_tanker.stop()
2008+
await phone_tanker.stop()
2009+
2010+
18892011
def test_prehash_password_empty() -> None:
18902012
with pytest.raises(error.InvalidArgument):
18912013
tankersdk.prehash_password("")

0 commit comments

Comments
 (0)