mgbam commited on
Commit
c33cb65
·
verified ·
1 Parent(s): d73f096

Update auth.py

Browse files
Files changed (1) hide show
  1. auth.py +119 -54
auth.py CHANGED
@@ -1,71 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
- from typing import Optional, Dict
 
 
3
  from authlib.integrations.requests_client import OAuth2Session
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  class OAuthManager:
6
- """
7
- Manages OAuth2 flows for third-party services: GitHub, Google Drive, Slack.
8
- """
9
- def __init__(self):
10
- self.providers: Dict[str, Dict] = {
11
- 'github': {
12
- 'client_id': os.getenv('GITHUB_CLIENT_ID'),
13
- 'client_secret': os.getenv('GITHUB_CLIENT_SECRET'),
14
- 'authorize_url': 'https://github.com/login/oauth/authorize',
15
- 'token_url': 'https://github.com/login/oauth/access_token',
16
- 'scope': 'repo read:org'
17
- },
18
- 'google': {
19
- 'client_id': os.getenv('GOOGLE_CLIENT_ID'),
20
- 'client_secret': os.getenv('GOOGLE_CLIENT_SECRET'),
21
- 'authorize_url': 'https://accounts.google.com/o/oauth2/auth',
22
- 'token_url': 'https://oauth2.googleapis.com/token',
23
- 'scope': 'openid email profile https://www.googleapis.com/auth/drive.readonly'
24
- },
25
- 'slack': {
26
- 'client_id': os.getenv('SLACK_CLIENT_ID'),
27
- 'client_secret': os.getenv('SLACK_CLIENT_SECRET'),
28
- 'authorize_url': 'https://slack.com/oauth/v2/authorize',
29
- 'token_url': 'https://slack.com/api/oauth.v2.access',
30
- 'scope': 'channels:read chat:write'
31
- }
32
- }
33
-
34
- def _create_session(self, provider: str, redirect_uri: str) -> OAuth2Session:
35
  cfg = self.providers.get(provider)
36
- if not cfg or not cfg['client_id'] or not cfg['client_secret']:
37
- raise RuntimeError(f"OAuth credentials for '{provider}' are not configured.")
 
 
 
 
 
38
  return OAuth2Session(
39
- cfg['client_id'],
40
- cfg['client_secret'],
41
- scope=cfg['scope'],
42
- redirect_uri=redirect_uri
43
  )
44
 
45
- def get_authorization_url(self, provider: str, redirect_uri: str, state: Optional[str] = None) -> (str, str):
 
 
 
46
  """
47
- Generate the OAuth2 authorization URL and state.
48
 
49
- Returns:
50
- (authorization_url, state)
51
  """
52
- session = self._create_session(provider, redirect_uri)
53
- url, state = session.create_authorization_url(self.providers[provider]['authorize_url'], state=state)
54
- return url, state
 
 
 
55
 
56
- def fetch_token(self, provider: str, redirect_uri: str, authorization_response: str) -> Dict:
 
 
57
  """
58
- Exchange the authorization response for an access token.
59
 
60
- Returns:
61
- Token dict containing access_token, refresh_token, expires_in, etc.
62
  """
63
- session = self._create_session(provider, redirect_uri)
64
- token = session.fetch_token(
65
- self.providers[provider]['token_url'],
66
- authorization_response=authorization_response
 
 
67
  )
68
- return token
69
 
70
- # Instantiate a global manager
71
- oauth_manager = OAuthManager()
 
 
1
+ # auth.py
2
+ """
3
+ Centralised OAuth 2.0 helper for GitHub, Google Drive and Slack.
4
+
5
+ Usage
6
+ -----
7
+ from auth import oauth_manager
8
+
9
+ # 1) Redirect user to consent page
10
+ auth_url, state = oauth_manager.get_authorization_url(
11
+ provider="github",
12
+ redirect_uri="https://your‑app.com/callback"
13
+ )
14
+
15
+ # 2) In your callback handler, exchange the code for a token
16
+ token = oauth_manager.fetch_token(
17
+ provider="github",
18
+ redirect_uri="https://your‑app.com/callback",
19
+ authorization_response=request.url # full URL with ?code=...
20
+ )
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
  import os
26
+ from dataclasses import dataclass
27
+ from typing import Dict, Optional, Tuple
28
+
29
  from authlib.integrations.requests_client import OAuth2Session
30
 
31
+
32
+ # ------------------------------------------------------------------ #
33
+ # 1  Provider configuration
34
+ # ------------------------------------------------------------------ #
35
+ @dataclass(frozen=True)
36
+ class ProviderConfig:
37
+ client_id: str | None
38
+ client_secret: str | None
39
+ authorize_url: str
40
+ token_url: str
41
+ scope: str
42
+
43
+
44
+ def _env(name: str) -> str | None:
45
+ """Shorthand for os.getenv with strip()."""
46
+ val = os.getenv(name)
47
+ return val.strip() if val else None
48
+
49
+
50
+ PROVIDERS: Dict[str, ProviderConfig] = {
51
+ "github": ProviderConfig(
52
+ client_id=_env("GITHUB_CLIENT_ID"),
53
+ client_secret=_env("GITHUB_CLIENT_SECRET"),
54
+ authorize_url="https://github.com/login/oauth/authorize",
55
+ token_url="https://github.com/login/oauth/access_token",
56
+ scope="repo read:org",
57
+ ),
58
+ "google": ProviderConfig(
59
+ client_id=_env("GOOGLE_CLIENT_ID"),
60
+ client_secret=_env("GOOGLE_CLIENT_SECRET"),
61
+ authorize_url="https://accounts.google.com/o/oauth2/auth",
62
+ token_url="https://oauth2.googleapis.com/token",
63
+ scope="openid email profile https://www.googleapis.com/auth/drive.readonly",
64
+ ),
65
+ "slack": ProviderConfig(
66
+ client_id=_env("SLACK_CLIENT_ID"),
67
+ client_secret=_env("SLACK_CLIENT_SECRET"),
68
+ authorize_url="https://slack.com/oauth/v2/authorize",
69
+ token_url="https://slack.com/api/oauth.v2.access",
70
+ scope="channels:read chat:write",
71
+ ),
72
+ }
73
+
74
+
75
+ # ------------------------------------------------------------------ #
76
+ # 2  OAuth manager
77
+ # ------------------------------------------------------------------ #
78
  class OAuthManager:
79
+ """Tiny wrapper around Authlib’s OAuth2Session per provider."""
80
+
81
+ def __init__(self, providers: Dict[str, ProviderConfig]):
82
+ self.providers = providers
83
+
84
+ # ---------- helpers -------------------------------------------------
85
+ def _session(self, provider: str, redirect_uri: str) -> OAuth2Session:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  cfg = self.providers.get(provider)
87
+ if not cfg:
88
+ raise KeyError(f"Unsupported provider '{provider}'.")
89
+ if not (cfg.client_id and cfg.client_secret):
90
+ raise RuntimeError(
91
+ f"OAuth credentials for '{provider}' are missing. "
92
+ "Set the *_CLIENT_ID and *_CLIENT_SECRET env‑vars."
93
+ )
94
  return OAuth2Session(
95
+ client_id=cfg.client_id,
96
+ client_secret=cfg.client_secret,
97
+ scope=cfg.scope,
98
+ redirect_uri=redirect_uri,
99
  )
100
 
101
+ # ---------- public API ----------------------------------------------
102
+ def get_authorization_url(
103
+ self, provider: str, redirect_uri: str, state: Optional[str] = None
104
+ ) -> Tuple[str, str]:
105
  """
106
+ Return (auth_url, state) for the given provider.
107
 
108
+ Pass the *state* back into `fetch_token` to mitigate CSRF.
 
109
  """
110
+ sess = self._session(provider, redirect_uri)
111
+ cfg = self.providers[provider]
112
+ auth_url, final_state = sess.create_authorization_url(
113
+ cfg.authorize_url, state=state
114
+ )
115
+ return auth_url, final_state
116
 
117
+ def fetch_token(
118
+ self, provider: str, redirect_uri: str, authorization_response: str
119
+ ) -> Dict:
120
  """
121
+ Exchange ?code=… for an access token.
122
 
123
+ Returns the token dict from Authlib (includes access_token,
124
+ refresh_token, expires_in, etc.).
125
  """
126
+ sess = self._session(provider, redirect_uri)
127
+ cfg = self.providers[provider]
128
+ return sess.fetch_token(
129
+ cfg.token_url,
130
+ authorization_response=authorization_response,
131
+ client_secret=cfg.client_secret, # some providers require it explicitly
132
  )
 
133
 
134
+
135
+ # Singleton instance used throughout the app
136
+ oauth_manager = OAuthManager(PROVIDERS)