Skip to content

Commit ef69a11

Browse files
committed
CM-55782: Add OIDC authentication
1 parent a8b1e4a commit ef69a11

File tree

12 files changed

+467
-22
lines changed

12 files changed

+467
-22
lines changed

cycode/cli/app.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ def app_callback(
110110
rich_help_panel=_AUTH_RICH_HELP_PANEL,
111111
),
112112
] = None,
113+
id_token: Annotated[
114+
Optional[str],
115+
typer.Option(
116+
help='Specify a Cycode OIDC ID token for this specific scan execution.',
117+
rich_help_panel=_AUTH_RICH_HELP_PANEL,
118+
),
119+
] = None,
113120
_: Annotated[
114121
Optional[bool],
115122
typer.Option(
@@ -152,6 +159,7 @@ def app_callback(
152159

153160
ctx.obj['client_id'] = client_id
154161
ctx.obj['client_secret'] = client_secret
162+
ctx.obj['id_token'] = id_token
155163

156164
ctx.obj['progress_bar'] = get_progress_bar(hidden=no_progress_meter, sections=SCAN_PROGRESS_BAR_SECTIONS)
157165

cycode/cli/apps/auth/auth_common.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, RequestHttpError
55
from cycode.cli.user_settings.credentials_manager import CredentialsManager
66
from cycode.cli.utils.jwt_utils import get_user_and_tenant_ids_from_access_token
7+
from cycode.cyclient.cycode_oidc_based_client import CycodeOidcBasedClient
78
from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient
89

910
if TYPE_CHECKING:
@@ -13,9 +14,23 @@
1314
def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]:
1415
printer = ctx.obj.get('console_printer')
1516

16-
client_id, client_secret = ctx.obj.get('client_id'), ctx.obj.get('client_secret')
17+
client_id = ctx.obj.get('client_id')
18+
client_secret = ctx.obj.get('client_secret')
19+
id_token = ctx.obj.get('id_token')
20+
21+
credentials_manager = CredentialsManager()
22+
23+
auth_info = _try_oidc_authorization(ctx, printer, client_id, id_token)
24+
if auth_info:
25+
return auth_info
26+
1727
if not client_id or not client_secret:
18-
client_id, client_secret = CredentialsManager().get_credentials()
28+
stored_client_id, stored_id_token = credentials_manager.get_oidc_credentials()
29+
auth_info = _try_oidc_authorization(ctx, printer, stored_client_id, stored_id_token)
30+
if auth_info:
31+
return auth_info
32+
33+
client_id, client_secret = credentials_manager.get_credentials()
1934

2035
if not client_id or not client_secret:
2136
return None
@@ -32,3 +47,23 @@ def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]:
3247
printer.print_exception()
3348

3449
return None
50+
51+
52+
def _try_oidc_authorization(
53+
ctx: 'Context', printer: any, client_id: Optional[str], id_token: Optional[str]
54+
) -> Optional[AuthInfo]:
55+
if not client_id or not id_token:
56+
return None
57+
58+
try:
59+
access_token = CycodeOidcBasedClient(client_id, id_token).get_access_token()
60+
if not access_token:
61+
return None
62+
63+
user_id, tenant_id = get_user_and_tenant_ids_from_access_token(access_token)
64+
return AuthInfo(user_id=user_id, tenant_id=tenant_id)
65+
except (RequestHttpError, HttpUnauthorizedError):
66+
if ctx:
67+
printer.print_exception()
68+
69+
return None

cycode/cli/apps/configure/configure_command.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
get_app_url_input,
88
get_client_id_input,
99
get_client_secret_input,
10+
get_id_token_input,
1011
)
1112
from cycode.cli.console import console
1213
from cycode.cli.utils.sentry import add_breadcrumb
@@ -32,6 +33,7 @@ def configure_command() -> None:
3233
* APP URL: The base URL for Cycode's web application (for on-premise or EU installations)
3334
* Client ID: Your Cycode client ID for authentication
3435
* Client Secret: Your Cycode client secret for authentication
36+
* ID Token: Your Cycode ID token for authentication
3537
3638
Example usage:
3739
* `cycode configure`: Start interactive configuration
@@ -55,15 +57,22 @@ def configure_command() -> None:
5557
config_updated = True
5658

5759
current_client_id, current_client_secret = CREDENTIALS_MANAGER.get_credentials_from_file()
60+
_, current_id_token = CREDENTIALS_MANAGER.get_oidc_credentials_from_file()
5861
client_id = get_client_id_input(current_client_id)
5962
client_secret = get_client_secret_input(current_client_secret)
63+
id_token = get_id_token_input(current_id_token)
6064

6165
credentials_updated = False
6266
if _should_update_value(current_client_id, client_id) or _should_update_value(current_client_secret, client_secret):
6367
credentials_updated = True
6468
CREDENTIALS_MANAGER.update_credentials(client_id, client_secret)
6569

70+
oidc_credentials_updated = False
71+
if _should_update_value(current_client_id, client_id) or _should_update_value(current_id_token, id_token):
72+
oidc_credentials_updated = True
73+
CREDENTIALS_MANAGER.update_oidc_credentials(client_id, id_token)
74+
6675
if config_updated:
6776
console.print(get_urls_update_result_message())
68-
if credentials_updated:
77+
if credentials_updated or oidc_credentials_updated:
6978
console.print(get_credentials_update_result_message())

cycode/cli/apps/configure/prompts.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,14 @@ def get_api_url_input(current_api_url: Optional[str]) -> str:
4646
default = current_api_url
4747

4848
return typer.prompt(text=prompt_text, default=default, type=str)
49+
50+
51+
def get_id_token_input(current_id_token: Optional[str]) -> Optional[str]:
52+
prompt_text = 'Cycode ID Token'
53+
54+
prompt_suffix = ' []: '
55+
if current_id_token:
56+
prompt_suffix = f' [{obfuscate_text(current_id_token)}]: '
57+
58+
new_id_token = typer.prompt(text=prompt_text, prompt_suffix=prompt_suffix, default='', show_default=False)
59+
return new_id_token or current_id_token

cycode/cli/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
# env vars
66
CYCODE_CLIENT_ID_ENV_VAR_NAME = 'CYCODE_CLIENT_ID'
77
CYCODE_CLIENT_SECRET_ENV_VAR_NAME = 'CYCODE_CLIENT_SECRET'
8+
CYCODE_ID_TOKEN_ENV_VAR_NAME = 'CYCODE_ID_TOKEN'

cycode/cli/user_settings/credentials_manager.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
from pathlib import Path
33
from typing import Optional
44

5-
from cycode.cli.config import CYCODE_CLIENT_ID_ENV_VAR_NAME, CYCODE_CLIENT_SECRET_ENV_VAR_NAME
5+
from cycode.cli.config import (
6+
CYCODE_CLIENT_ID_ENV_VAR_NAME,
7+
CYCODE_CLIENT_SECRET_ENV_VAR_NAME,
8+
CYCODE_ID_TOKEN_ENV_VAR_NAME,
9+
)
610
from cycode.cli.user_settings.base_file_manager import BaseFileManager
711
from cycode.cli.user_settings.jwt_creator import JwtCreator
812
from cycode.cli.utils.sentry import setup_scope_from_access_token
@@ -15,6 +19,7 @@ class CredentialsManager(BaseFileManager):
1519

1620
CLIENT_ID_FIELD_NAME: str = 'cycode_client_id'
1721
CLIENT_SECRET_FIELD_NAME: str = 'cycode_client_secret'
22+
ID_TOKEN_FIELD_NAME: str = 'cycode_id_token'
1823
ACCESS_TOKEN_FIELD_NAME: str = 'cycode_access_token'
1924
ACCESS_TOKEN_EXPIRES_IN_FIELD_NAME: str = 'cycode_access_token_expires_in'
2025
ACCESS_TOKEN_CREATOR_FIELD_NAME: str = 'cycode_access_token_creator'
@@ -38,6 +43,25 @@ def get_credentials_from_file(self) -> tuple[Optional[str], Optional[str]]:
3843
client_secret = file_content.get(self.CLIENT_SECRET_FIELD_NAME)
3944
return client_id, client_secret
4045

46+
def get_oidc_credentials_from_file(self) -> tuple[Optional[str], Optional[str]]:
47+
file_content = self.read_file()
48+
client_id = file_content.get(self.CLIENT_ID_FIELD_NAME)
49+
id_token = file_content.get(self.ID_TOKEN_FIELD_NAME)
50+
return client_id, id_token
51+
52+
def get_oidc_credentials(self) -> tuple[Optional[str], Optional[str]]:
53+
client_id = os.getenv(CYCODE_CLIENT_ID_ENV_VAR_NAME)
54+
id_token = os.getenv(CYCODE_ID_TOKEN_ENV_VAR_NAME)
55+
56+
if client_id is not None and id_token is not None:
57+
return client_id, id_token
58+
59+
return self.get_oidc_credentials_from_file()
60+
61+
def update_oidc_credentials(self, client_id: str, id_token: str) -> None:
62+
file_content_to_update = {self.CLIENT_ID_FIELD_NAME: client_id, self.ID_TOKEN_FIELD_NAME: id_token}
63+
self.write_content_to_file(file_content_to_update)
64+
4165
def update_credentials(self, client_id: str, client_secret: str) -> None:
4266
file_content_to_update = {self.CLIENT_ID_FIELD_NAME: client_id, self.CLIENT_SECRET_FIELD_NAME: client_secret}
4367
self.write_content_to_file(file_content_to_update)

cycode/cli/utils/get_api_client.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,58 @@
1414

1515

1616
def _get_cycode_client(
17-
create_client_func: callable, client_id: Optional[str], client_secret: Optional[str], hide_response_log: bool
17+
create_client_func: callable,
18+
client_id: Optional[str],
19+
client_secret: Optional[str],
20+
hide_response_log: bool,
21+
id_token: Optional[str] = None,
1822
) -> Union['ScanClient', 'ReportClient']:
23+
if id_token:
24+
if not client_id:
25+
raise click.ClickException('Cycode client id needed for OIDC authentication.')
26+
return create_client_func(client_id, None, hide_response_log, id_token)
27+
1928
if not client_id or not client_secret:
29+
oidc_client_id, oidc_id_token = _get_configured_oidc_credentials()
30+
if oidc_client_id and oidc_id_token:
31+
return create_client_func(oidc_client_id, None, hide_response_log, oidc_id_token)
32+
2033
client_id, client_secret = _get_configured_credentials()
2134
if not client_id:
2235
raise click.ClickException('Cycode client id needed.')
2336
if not client_secret:
2437
raise click.ClickException('Cycode client secret is needed.')
2538

26-
return create_client_func(client_id, client_secret, hide_response_log)
39+
return create_client_func(client_id, client_secret, hide_response_log, None)
2740

2841

2942
def get_scan_cycode_client(ctx: 'typer.Context') -> 'ScanClient':
3043
client_id = ctx.obj.get('client_id')
3144
client_secret = ctx.obj.get('client_secret')
45+
id_token = ctx.obj.get('id_token')
3246
hide_response_log = not ctx.obj.get('show_secret', False)
33-
return _get_cycode_client(create_scan_client, client_id, client_secret, hide_response_log)
47+
return _get_cycode_client(create_scan_client, client_id, client_secret, hide_response_log, id_token)
3448

3549

3650
def get_report_cycode_client(ctx: 'typer.Context', hide_response_log: bool = True) -> 'ReportClient':
3751
client_id = ctx.obj.get('client_id')
3852
client_secret = ctx.obj.get('client_secret')
39-
return _get_cycode_client(create_report_client, client_id, client_secret, hide_response_log)
53+
id_token = ctx.obj.get('id_token')
54+
return _get_cycode_client(create_report_client, client_id, client_secret, hide_response_log, id_token)
4055

4156

4257
def get_import_sbom_cycode_client(ctx: 'typer.Context', hide_response_log: bool = True) -> 'ImportSbomClient':
4358
client_id = ctx.obj.get('client_id')
4459
client_secret = ctx.obj.get('client_secret')
45-
return _get_cycode_client(create_import_sbom_client, client_id, client_secret, hide_response_log)
60+
id_token = ctx.obj.get('id_token')
61+
return _get_cycode_client(create_import_sbom_client, client_id, client_secret, hide_response_log, id_token)
4662

4763

4864
def _get_configured_credentials() -> tuple[str, str]:
4965
credentials_manager = CredentialsManager()
5066
return credentials_manager.get_credentials()
67+
68+
69+
def _get_configured_oidc_credentials() -> tuple[Optional[str], Optional[str]]:
70+
credentials_manager = CredentialsManager()
71+
return credentials_manager.get_oidc_credentials()

cycode/cyclient/client_creator.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,51 @@
1+
from typing import Optional
2+
13
from cycode.cyclient.config import dev_mode
24
from cycode.cyclient.config_dev import DEV_CYCODE_API_URL
35
from cycode.cyclient.cycode_dev_based_client import CycodeDevBasedClient
6+
from cycode.cyclient.cycode_oidc_based_client import CycodeOidcBasedClient
47
from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient
58
from cycode.cyclient.import_sbom_client import ImportSbomClient
69
from cycode.cyclient.report_client import ReportClient
710
from cycode.cyclient.scan_client import ScanClient
811
from cycode.cyclient.scan_config_base import DefaultScanConfig, DevScanConfig
912

1013

11-
def create_scan_client(client_id: str, client_secret: str, hide_response_log: bool) -> ScanClient:
14+
def create_scan_client(
15+
client_id: str, client_secret: Optional[str] = None, hide_response_log: bool = False, id_token: Optional[str] = None
16+
) -> ScanClient:
1217
if dev_mode:
1318
client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
1419
scan_config = DevScanConfig()
1520
else:
16-
client = CycodeTokenBasedClient(client_id, client_secret)
21+
if id_token:
22+
client = CycodeOidcBasedClient(client_id, id_token)
23+
else:
24+
client = CycodeTokenBasedClient(client_id, client_secret)
1725
scan_config = DefaultScanConfig()
1826

1927
return ScanClient(client, scan_config, hide_response_log)
2028

2129

22-
def create_report_client(client_id: str, client_secret: str, _: bool) -> ReportClient:
23-
client = CycodeDevBasedClient(DEV_CYCODE_API_URL) if dev_mode else CycodeTokenBasedClient(client_id, client_secret)
30+
def create_report_client(
31+
client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None
32+
) -> ReportClient:
33+
if dev_mode:
34+
client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
35+
elif id_token:
36+
client = CycodeOidcBasedClient(client_id, id_token)
37+
else:
38+
client = CycodeTokenBasedClient(client_id, client_secret)
2439
return ReportClient(client)
2540

2641

27-
def create_import_sbom_client(client_id: str, client_secret: str, _: bool) -> ImportSbomClient:
28-
client = CycodeDevBasedClient(DEV_CYCODE_API_URL) if dev_mode else CycodeTokenBasedClient(client_id, client_secret)
42+
def create_import_sbom_client(
43+
client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None
44+
) -> ImportSbomClient:
45+
if dev_mode:
46+
client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
47+
elif id_token:
48+
client = CycodeOidcBasedClient(client_id, id_token)
49+
else:
50+
client = CycodeTokenBasedClient(client_id, client_secret)
2951
return ImportSbomClient(client)

0 commit comments

Comments
 (0)