fief_client.integrations.cli

CLI integration.

  1"""CLI integration."""
  2import functools
  3import http
  4import http.server
  5import json
  6import pathlib
  7import queue
  8import typing
  9import urllib.parse
 10import webbrowser
 11
 12from halo import Halo
 13
 14from fief_client import (
 15    Fief,
 16    FiefAccessTokenExpired,
 17    FiefAccessTokenInfo,
 18    FiefTokenResponse,
 19    FiefUserInfo,
 20)
 21from fief_client.pkce import get_code_challenge, get_code_verifier
 22
 23
 24class FiefAuthError(Exception):
 25    """
 26    Base error for FiefAuth integration.
 27    """
 28
 29
 30class FiefAuthNotAuthenticatedError(FiefAuthError):
 31    """
 32    The user is not authenticated.
 33    """
 34
 35    pass
 36
 37
 38class FiefAuthAuthorizationCodeMissingError(FiefAuthError):
 39    """
 40    The authorization code was not found in the redirection URL.
 41    """
 42
 43    pass
 44
 45
 46class FiefAuthRefreshTokenMissingError(FiefAuthError):
 47    """
 48    The refresh token is missing in the saved credentials.
 49    """
 50
 51    pass
 52
 53
 54class CallbackHTTPServer(http.server.ThreadingHTTPServer):
 55    pass
 56
 57
 58class CallbackHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
 59    def __init__(
 60        self,
 61        *args,
 62        queue: "queue.Queue[str]",
 63        render_success_page,
 64        render_error_page,
 65        **kwargs,
 66    ) -> None:
 67        self.queue = queue
 68        self.render_success_page = render_success_page
 69        self.render_error_page = render_error_page
 70        super().__init__(*args, **kwargs)
 71
 72    def log_message(self, format: str, *args: typing.Any) -> None:
 73        pass
 74
 75    def do_GET(self):
 76        parsed_url = urllib.parse.urlparse(self.path)
 77        query_params = urllib.parse.parse_qs(parsed_url.query)
 78
 79        try:
 80            code = query_params["code"][0]
 81        except (KeyError, IndexError):
 82            output = self.render_error_page(query_params).encode("utf-8")
 83            self.send_response(http.HTTPStatus.BAD_REQUEST)
 84            self.send_header("Content-type", "text/html; charset=utf-8")
 85            self.send_header("Content-Length", str(len(output)))
 86            self.end_headers()
 87            self.wfile.write(output)
 88        else:
 89            self.queue.put(code)
 90
 91            output = self.render_success_page().encode("utf-8")
 92            self.send_response(http.HTTPStatus.OK)
 93            self.send_header("Content-type", "text/html; charset=utf-8")
 94            self.send_header("Content-Length", str(len(output)))
 95            self.end_headers()
 96            self.wfile.write(output)
 97
 98        self.server.shutdown()
 99
100
101class FiefAuth:
102    """
103    Helper class to integrate Fief authentication in a CLI tool.
104
105    **Example:**
106
107    ```py
108    from fief_client import Fief
109    from fief_client.integrations.cli import FiefAuth
110
111    fief = Fief(
112        "https://example.fief.dev",
113        "YOUR_CLIENT_ID",
114    )
115    auth = FiefAuth(fief, "./credentials.json")
116    ```
117    """
118
119    _userinfo: typing.Optional[FiefUserInfo] = None
120    _tokens: typing.Optional[FiefTokenResponse] = None
121
122    def __init__(self, client: Fief, credentials_path: str) -> None:
123        """
124        :param client: Instance of a Fief client.
125        :param credentials_path: Path where the credentials will be stored on the user machine.
126        We recommend you to use a library like [appdir](https://github.com/ActiveState/appdirs)
127        to determine a reasonable path depending on the user's operating system.
128        """
129        self.client = client
130        self.credentials_path = pathlib.Path(credentials_path)
131        self._load_stored_credentials()
132
133    def access_token_info(self, refresh: bool = True) -> FiefAccessTokenInfo:
134        """
135        Return credentials information saved on disk.
136
137        Optionally, it can automatically get a fresh `access_token` if
138        the saved one is expired.
139
140        :param refresh: Whether the client should automatically refresh the token.
141        Defaults to `True`.
142
143        :raises: `FiefAuthNotAuthenticatedError` if the user is not authenticated.
144        :raises: `fief_client.FiefAccessTokenExpired` if the access token is expired and automatic refresh is disabled.
145        """
146        if self._tokens is None:
147            raise FiefAuthNotAuthenticatedError()
148
149        access_token = self._tokens["access_token"]
150        try:
151            return self.client.validate_access_token(access_token)
152        except FiefAccessTokenExpired:
153            if refresh:
154                self._refresh_access_token()
155                return self.access_token_info()
156            raise
157
158    def current_user(self, refresh: bool = False) -> FiefUserInfo:
159        """
160        Return user information saved on disk.
161
162        Optionally, it can automatically refresh it from the server if there
163        is a valid access token.
164
165        :param refresh: Whether the client should refresh the user information.
166        Defaults to `False`.
167
168        :raises: `FiefAuthNotAuthenticatedError` if the user is not authenticated.
169        """
170        if self._tokens is None or self._userinfo is None:
171            raise FiefAuthNotAuthenticatedError()
172        if refresh:
173            access_token_info = self.access_token_info()
174            userinfo = self.client.userinfo(access_token_info["access_token"])
175            self._save_credentials(self._tokens, userinfo)
176        return self._userinfo
177
178    def authorize(
179        self,
180        server_address: typing.Tuple[str, int] = ("localhost", 51562),
181        redirect_path: str = "/callback",
182        *,
183        scope: typing.Optional[typing.List[str]] = None,
184        lang: typing.Optional[str] = None,
185        extras_params: typing.Optional[typing.Mapping[str, str]] = None,
186    ) -> typing.Tuple[FiefTokenResponse, FiefUserInfo]:
187        """
188        Perform a user authentication with the Fief server.
189
190        It'll automatically open the user's default browser and redirect them
191        to the Fief authorization page.
192
193        Under the hood, the client opens a temporary web server.
194
195        After a successful authentication, Fief will redirect to this web server
196        so the client can catch the authorization code and generate a valid access token.
197
198        Finally, it'll automatically save the credentials on disk.
199
200        :param server_address: The address of the temporary web server the client should open.
201        It's a tuple composed of the IP and the port. Defaults to `("localhost", 51562)`.
202        :param redirect_path: Redirect URI where Fief will redirect after a successful authentication.
203        Defaults to `/callback`.
204        :param scope: Optional list of scopes to ask for.
205        The client will **always** ask at least for `openid` and `offline_access`.
206        :param lang: Optional parameter to set the user locale on the authentication pages.
207        Should be a valid [RFC 3066](https://www.rfc-editor.org/rfc/rfc3066) language identifier, like `fr` or `pt-PT`.
208        :param extras_params: Optional dictionary containing [specific parameters](https://docs.fief.dev/going-further/authorize-url/).
209
210        **Example:**
211
212        ```py
213        tokens, userinfo = auth.authorize()
214        ```
215        """
216        redirect_uri = f"http://{server_address[0]}:{server_address[1]}{redirect_path}"
217
218        scope_set: typing.Set[str] = set(scope) if scope else set()
219        scope_set.add("openid")
220        scope_set.add("offline_access")
221
222        code_verifier = get_code_verifier()
223        code_challenge = get_code_challenge(code_verifier)
224
225        authorization_url = self.client.auth_url(
226            redirect_uri,
227            scope=list(scope_set),
228            code_challenge=code_challenge,
229            code_challenge_method="S256",
230            lang=lang,
231            extras_params=extras_params,
232        )
233        webbrowser.open(authorization_url)
234
235        spinner = Halo(
236            text="Please complete authentication in your browser.", spinner="dots"
237        )
238        spinner.start()
239
240        code_queue: queue.Queue[str] = queue.Queue()
241        server = CallbackHTTPServer(
242            server_address,
243            functools.partial(
244                CallbackHTTPRequestHandler,
245                queue=code_queue,
246                render_success_page=self.render_success_page,
247                render_error_page=self.render_error_page,
248            ),
249        )
250
251        server.serve_forever()
252
253        try:
254            code = code_queue.get(block=False)
255        except queue.Empty as e:
256            raise FiefAuthAuthorizationCodeMissingError() from e
257
258        spinner.text = "Getting a token..."
259
260        tokens, userinfo = self.client.auth_callback(
261            code, redirect_uri, code_verifier=code_verifier
262        )
263        self._save_credentials(tokens, userinfo)
264
265        spinner.succeed("Successfully authenticated")
266
267        return tokens, userinfo
268
269    def render_success_page(self) -> str:
270        """
271        Generate the HTML page that'll be shown to the user after a successful redirection.
272
273        By default, it just tells the user that it can go back to the CLI.
274
275        You can override this method if you want to customize this page.
276        """
277        return f"""
278        <html>
279            <head>
280                <link href="{self.client.base_url}/static/auth.css" rel="stylesheet">
281            </head>
282            <body class="antialiased">
283                <main>
284                    <div class="relative flex">
285                        <div class="w-full">
286                            <div class="min-h-screen h-full flex flex flex-col after:flex-1">
287                                <div class="flex-1"></div>
288                                <div class="w-full max-w-sm mx-auto px-4 py-8 text-center">
289                                    <h1 class="text-3xl text-accent font-bold mb-6">Done! You can go back to your terminal!</h1>
290                                </div>
291                            </div>
292                        </div>
293                    </div>
294                </main>
295                <script>
296                    window.addEventListener("DOMContentLoaded", () => {{
297                        setTimeout(() => {{
298                            window.close();
299                        }}, 5000);
300                    }});
301                </script>
302            </body>
303        </html>
304        """
305
306    def render_error_page(self, query_params: typing.Dict[str, typing.Any]) -> str:
307        """
308        Generate the HTML page that'll be shown to the user when something goes wrong during redirection.
309
310        You can override this method if you want to customize this page.
311        """
312        return f"""
313        <html>
314            <head>
315                <link href="{self.client.base_url}/static/auth.css" rel="stylesheet">
316            </head>
317            <body class="antialiased">
318                <main>
319                    <div class="relative flex">
320                        <div class="w-full">
321                            <div class="min-h-screen h-full flex flex flex-col after:flex-1">
322                                <div class="flex-1"></div>
323                                <div class="w-full max-w-sm mx-auto px-4 py-8 text-center">
324                                    <h1 class="text-3xl text-accent font-bold mb-6">Something went wrong! You're not authenticated.</h1>
325                                    <p>Error detail: {json.dumps(query_params)}</p>
326                                </div>
327                            </div>
328                        </div>
329                    </div>
330                </main>
331            </body>
332        </html>
333        """
334
335    def _refresh_access_token(self):
336        refresh_token = self._tokens.get("refresh_token")
337        if refresh_token is None:
338            raise FiefAuthRefreshTokenMissingError()
339        tokens, userinfo = self.client.auth_refresh_token(refresh_token)
340        self._save_credentials(tokens, userinfo)
341
342    def _load_stored_credentials(self):
343        if self.credentials_path.exists():
344            with open(self.credentials_path) as file:
345                try:
346                    data = json.loads(file.read())
347                    self._userinfo = data["userinfo"]
348                    self._tokens = data["tokens"]
349                except json.decoder.JSONDecodeError:
350                    pass
351
352    def _save_credentials(self, tokens: FiefTokenResponse, userinfo: FiefUserInfo):
353        self._tokens = tokens
354        self._userinfo = userinfo
355        with open(self.credentials_path, "w") as file:
356            data = {"userinfo": userinfo, "tokens": tokens}
357            file.write(json.dumps(data))
358
359
360__all__ = [
361    "FiefAuth",
362    "FiefAuthError",
363    "FiefAuthNotAuthenticatedError",
364    "FiefAuthAuthorizationCodeMissingError",
365    "FiefAuthRefreshTokenMissingError",
366]
class FiefAuth:
102class FiefAuth:
103    """
104    Helper class to integrate Fief authentication in a CLI tool.
105
106    **Example:**
107
108    ```py
109    from fief_client import Fief
110    from fief_client.integrations.cli import FiefAuth
111
112    fief = Fief(
113        "https://example.fief.dev",
114        "YOUR_CLIENT_ID",
115    )
116    auth = FiefAuth(fief, "./credentials.json")
117    ```
118    """
119
120    _userinfo: typing.Optional[FiefUserInfo] = None
121    _tokens: typing.Optional[FiefTokenResponse] = None
122
123    def __init__(self, client: Fief, credentials_path: str) -> None:
124        """
125        :param client: Instance of a Fief client.
126        :param credentials_path: Path where the credentials will be stored on the user machine.
127        We recommend you to use a library like [appdir](https://github.com/ActiveState/appdirs)
128        to determine a reasonable path depending on the user's operating system.
129        """
130        self.client = client
131        self.credentials_path = pathlib.Path(credentials_path)
132        self._load_stored_credentials()
133
134    def access_token_info(self, refresh: bool = True) -> FiefAccessTokenInfo:
135        """
136        Return credentials information saved on disk.
137
138        Optionally, it can automatically get a fresh `access_token` if
139        the saved one is expired.
140
141        :param refresh: Whether the client should automatically refresh the token.
142        Defaults to `True`.
143
144        :raises: `FiefAuthNotAuthenticatedError` if the user is not authenticated.
145        :raises: `fief_client.FiefAccessTokenExpired` if the access token is expired and automatic refresh is disabled.
146        """
147        if self._tokens is None:
148            raise FiefAuthNotAuthenticatedError()
149
150        access_token = self._tokens["access_token"]
151        try:
152            return self.client.validate_access_token(access_token)
153        except FiefAccessTokenExpired:
154            if refresh:
155                self._refresh_access_token()
156                return self.access_token_info()
157            raise
158
159    def current_user(self, refresh: bool = False) -> FiefUserInfo:
160        """
161        Return user information saved on disk.
162
163        Optionally, it can automatically refresh it from the server if there
164        is a valid access token.
165
166        :param refresh: Whether the client should refresh the user information.
167        Defaults to `False`.
168
169        :raises: `FiefAuthNotAuthenticatedError` if the user is not authenticated.
170        """
171        if self._tokens is None or self._userinfo is None:
172            raise FiefAuthNotAuthenticatedError()
173        if refresh:
174            access_token_info = self.access_token_info()
175            userinfo = self.client.userinfo(access_token_info["access_token"])
176            self._save_credentials(self._tokens, userinfo)
177        return self._userinfo
178
179    def authorize(
180        self,
181        server_address: typing.Tuple[str, int] = ("localhost", 51562),
182        redirect_path: str = "/callback",
183        *,
184        scope: typing.Optional[typing.List[str]] = None,
185        lang: typing.Optional[str] = None,
186        extras_params: typing.Optional[typing.Mapping[str, str]] = None,
187    ) -> typing.Tuple[FiefTokenResponse, FiefUserInfo]:
188        """
189        Perform a user authentication with the Fief server.
190
191        It'll automatically open the user's default browser and redirect them
192        to the Fief authorization page.
193
194        Under the hood, the client opens a temporary web server.
195
196        After a successful authentication, Fief will redirect to this web server
197        so the client can catch the authorization code and generate a valid access token.
198
199        Finally, it'll automatically save the credentials on disk.
200
201        :param server_address: The address of the temporary web server the client should open.
202        It's a tuple composed of the IP and the port. Defaults to `("localhost", 51562)`.
203        :param redirect_path: Redirect URI where Fief will redirect after a successful authentication.
204        Defaults to `/callback`.
205        :param scope: Optional list of scopes to ask for.
206        The client will **always** ask at least for `openid` and `offline_access`.
207        :param lang: Optional parameter to set the user locale on the authentication pages.
208        Should be a valid [RFC 3066](https://www.rfc-editor.org/rfc/rfc3066) language identifier, like `fr` or `pt-PT`.
209        :param extras_params: Optional dictionary containing [specific parameters](https://docs.fief.dev/going-further/authorize-url/).
210
211        **Example:**
212
213        ```py
214        tokens, userinfo = auth.authorize()
215        ```
216        """
217        redirect_uri = f"http://{server_address[0]}:{server_address[1]}{redirect_path}"
218
219        scope_set: typing.Set[str] = set(scope) if scope else set()
220        scope_set.add("openid")
221        scope_set.add("offline_access")
222
223        code_verifier = get_code_verifier()
224        code_challenge = get_code_challenge(code_verifier)
225
226        authorization_url = self.client.auth_url(
227            redirect_uri,
228            scope=list(scope_set),
229            code_challenge=code_challenge,
230            code_challenge_method="S256",
231            lang=lang,
232            extras_params=extras_params,
233        )
234        webbrowser.open(authorization_url)
235
236        spinner = Halo(
237            text="Please complete authentication in your browser.", spinner="dots"
238        )
239        spinner.start()
240
241        code_queue: queue.Queue[str] = queue.Queue()
242        server = CallbackHTTPServer(
243            server_address,
244            functools.partial(
245                CallbackHTTPRequestHandler,
246                queue=code_queue,
247                render_success_page=self.render_success_page,
248                render_error_page=self.render_error_page,
249            ),
250        )
251
252        server.serve_forever()
253
254        try:
255            code = code_queue.get(block=False)
256        except queue.Empty as e:
257            raise FiefAuthAuthorizationCodeMissingError() from e
258
259        spinner.text = "Getting a token..."
260
261        tokens, userinfo = self.client.auth_callback(
262            code, redirect_uri, code_verifier=code_verifier
263        )
264        self._save_credentials(tokens, userinfo)
265
266        spinner.succeed("Successfully authenticated")
267
268        return tokens, userinfo
269
270    def render_success_page(self) -> str:
271        """
272        Generate the HTML page that'll be shown to the user after a successful redirection.
273
274        By default, it just tells the user that it can go back to the CLI.
275
276        You can override this method if you want to customize this page.
277        """
278        return f"""
279        <html>
280            <head>
281                <link href="{self.client.base_url}/static/auth.css" rel="stylesheet">
282            </head>
283            <body class="antialiased">
284                <main>
285                    <div class="relative flex">
286                        <div class="w-full">
287                            <div class="min-h-screen h-full flex flex flex-col after:flex-1">
288                                <div class="flex-1"></div>
289                                <div class="w-full max-w-sm mx-auto px-4 py-8 text-center">
290                                    <h1 class="text-3xl text-accent font-bold mb-6">Done! You can go back to your terminal!</h1>
291                                </div>
292                            </div>
293                        </div>
294                    </div>
295                </main>
296                <script>
297                    window.addEventListener("DOMContentLoaded", () => {{
298                        setTimeout(() => {{
299                            window.close();
300                        }}, 5000);
301                    }});
302                </script>
303            </body>
304        </html>
305        """
306
307    def render_error_page(self, query_params: typing.Dict[str, typing.Any]) -> str:
308        """
309        Generate the HTML page that'll be shown to the user when something goes wrong during redirection.
310
311        You can override this method if you want to customize this page.
312        """
313        return f"""
314        <html>
315            <head>
316                <link href="{self.client.base_url}/static/auth.css" rel="stylesheet">
317            </head>
318            <body class="antialiased">
319                <main>
320                    <div class="relative flex">
321                        <div class="w-full">
322                            <div class="min-h-screen h-full flex flex flex-col after:flex-1">
323                                <div class="flex-1"></div>
324                                <div class="w-full max-w-sm mx-auto px-4 py-8 text-center">
325                                    <h1 class="text-3xl text-accent font-bold mb-6">Something went wrong! You're not authenticated.</h1>
326                                    <p>Error detail: {json.dumps(query_params)}</p>
327                                </div>
328                            </div>
329                        </div>
330                    </div>
331                </main>
332            </body>
333        </html>
334        """
335
336    def _refresh_access_token(self):
337        refresh_token = self._tokens.get("refresh_token")
338        if refresh_token is None:
339            raise FiefAuthRefreshTokenMissingError()
340        tokens, userinfo = self.client.auth_refresh_token(refresh_token)
341        self._save_credentials(tokens, userinfo)
342
343    def _load_stored_credentials(self):
344        if self.credentials_path.exists():
345            with open(self.credentials_path) as file:
346                try:
347                    data = json.loads(file.read())
348                    self._userinfo = data["userinfo"]
349                    self._tokens = data["tokens"]
350                except json.decoder.JSONDecodeError:
351                    pass
352
353    def _save_credentials(self, tokens: FiefTokenResponse, userinfo: FiefUserInfo):
354        self._tokens = tokens
355        self._userinfo = userinfo
356        with open(self.credentials_path, "w") as file:
357            data = {"userinfo": userinfo, "tokens": tokens}
358            file.write(json.dumps(data))

Helper class to integrate Fief authentication in a CLI tool.

Example:

from fief_client import Fief
from fief_client.integrations.cli import FiefAuth

fief = Fief(
    "https://example.fief.dev",
    "YOUR_CLIENT_ID",
)
auth = FiefAuth(fief, "./credentials.json")
FiefAuth(client: fief_client.client.Fief, credentials_path: str)
123    def __init__(self, client: Fief, credentials_path: str) -> None:
124        """
125        :param client: Instance of a Fief client.
126        :param credentials_path: Path where the credentials will be stored on the user machine.
127        We recommend you to use a library like [appdir](https://github.com/ActiveState/appdirs)
128        to determine a reasonable path depending on the user's operating system.
129        """
130        self.client = client
131        self.credentials_path = pathlib.Path(credentials_path)
132        self._load_stored_credentials()
Parameters
  • client: Instance of a Fief client.
  • credentials_path: Path where the credentials will be stored on the user machine. We recommend you to use a library like appdir to determine a reasonable path depending on the user's operating system.
client
credentials_path
def access_token_info(self, refresh: bool = True) -> fief_client.client.FiefAccessTokenInfo:
134    def access_token_info(self, refresh: bool = True) -> FiefAccessTokenInfo:
135        """
136        Return credentials information saved on disk.
137
138        Optionally, it can automatically get a fresh `access_token` if
139        the saved one is expired.
140
141        :param refresh: Whether the client should automatically refresh the token.
142        Defaults to `True`.
143
144        :raises: `FiefAuthNotAuthenticatedError` if the user is not authenticated.
145        :raises: `fief_client.FiefAccessTokenExpired` if the access token is expired and automatic refresh is disabled.
146        """
147        if self._tokens is None:
148            raise FiefAuthNotAuthenticatedError()
149
150        access_token = self._tokens["access_token"]
151        try:
152            return self.client.validate_access_token(access_token)
153        except FiefAccessTokenExpired:
154            if refresh:
155                self._refresh_access_token()
156                return self.access_token_info()
157            raise

Return credentials information saved on disk.

Optionally, it can automatically get a fresh access_token if the saved one is expired.

Parameters
  • refresh: Whether the client should automatically refresh the token. Defaults to True.
Raises
def current_user(self, refresh: bool = False) -> fief_client.client.FiefUserInfo:
159    def current_user(self, refresh: bool = False) -> FiefUserInfo:
160        """
161        Return user information saved on disk.
162
163        Optionally, it can automatically refresh it from the server if there
164        is a valid access token.
165
166        :param refresh: Whether the client should refresh the user information.
167        Defaults to `False`.
168
169        :raises: `FiefAuthNotAuthenticatedError` if the user is not authenticated.
170        """
171        if self._tokens is None or self._userinfo is None:
172            raise FiefAuthNotAuthenticatedError()
173        if refresh:
174            access_token_info = self.access_token_info()
175            userinfo = self.client.userinfo(access_token_info["access_token"])
176            self._save_credentials(self._tokens, userinfo)
177        return self._userinfo

Return user information saved on disk.

Optionally, it can automatically refresh it from the server if there is a valid access token.

Parameters
  • refresh: Whether the client should refresh the user information. Defaults to False.
Raises
def authorize( self, server_address: Tuple[str, int] = ('localhost', 51562), redirect_path: str = '/callback', *, scope: Optional[List[str]] = None, lang: Optional[str] = None, extras_params: Optional[Mapping[str, str]] = None) -> Tuple[fief_client.client.FiefTokenResponse, fief_client.client.FiefUserInfo]:
179    def authorize(
180        self,
181        server_address: typing.Tuple[str, int] = ("localhost", 51562),
182        redirect_path: str = "/callback",
183        *,
184        scope: typing.Optional[typing.List[str]] = None,
185        lang: typing.Optional[str] = None,
186        extras_params: typing.Optional[typing.Mapping[str, str]] = None,
187    ) -> typing.Tuple[FiefTokenResponse, FiefUserInfo]:
188        """
189        Perform a user authentication with the Fief server.
190
191        It'll automatically open the user's default browser and redirect them
192        to the Fief authorization page.
193
194        Under the hood, the client opens a temporary web server.
195
196        After a successful authentication, Fief will redirect to this web server
197        so the client can catch the authorization code and generate a valid access token.
198
199        Finally, it'll automatically save the credentials on disk.
200
201        :param server_address: The address of the temporary web server the client should open.
202        It's a tuple composed of the IP and the port. Defaults to `("localhost", 51562)`.
203        :param redirect_path: Redirect URI where Fief will redirect after a successful authentication.
204        Defaults to `/callback`.
205        :param scope: Optional list of scopes to ask for.
206        The client will **always** ask at least for `openid` and `offline_access`.
207        :param lang: Optional parameter to set the user locale on the authentication pages.
208        Should be a valid [RFC 3066](https://www.rfc-editor.org/rfc/rfc3066) language identifier, like `fr` or `pt-PT`.
209        :param extras_params: Optional dictionary containing [specific parameters](https://docs.fief.dev/going-further/authorize-url/).
210
211        **Example:**
212
213        ```py
214        tokens, userinfo = auth.authorize()
215        ```
216        """
217        redirect_uri = f"http://{server_address[0]}:{server_address[1]}{redirect_path}"
218
219        scope_set: typing.Set[str] = set(scope) if scope else set()
220        scope_set.add("openid")
221        scope_set.add("offline_access")
222
223        code_verifier = get_code_verifier()
224        code_challenge = get_code_challenge(code_verifier)
225
226        authorization_url = self.client.auth_url(
227            redirect_uri,
228            scope=list(scope_set),
229            code_challenge=code_challenge,
230            code_challenge_method="S256",
231            lang=lang,
232            extras_params=extras_params,
233        )
234        webbrowser.open(authorization_url)
235
236        spinner = Halo(
237            text="Please complete authentication in your browser.", spinner="dots"
238        )
239        spinner.start()
240
241        code_queue: queue.Queue[str] = queue.Queue()
242        server = CallbackHTTPServer(
243            server_address,
244            functools.partial(
245                CallbackHTTPRequestHandler,
246                queue=code_queue,
247                render_success_page=self.render_success_page,
248                render_error_page=self.render_error_page,
249            ),
250        )
251
252        server.serve_forever()
253
254        try:
255            code = code_queue.get(block=False)
256        except queue.Empty as e:
257            raise FiefAuthAuthorizationCodeMissingError() from e
258
259        spinner.text = "Getting a token..."
260
261        tokens, userinfo = self.client.auth_callback(
262            code, redirect_uri, code_verifier=code_verifier
263        )
264        self._save_credentials(tokens, userinfo)
265
266        spinner.succeed("Successfully authenticated")
267
268        return tokens, userinfo

Perform a user authentication with the Fief server.

It'll automatically open the user's default browser and redirect them to the Fief authorization page.

Under the hood, the client opens a temporary web server.

After a successful authentication, Fief will redirect to this web server so the client can catch the authorization code and generate a valid access token.

Finally, it'll automatically save the credentials on disk.

Parameters
  • server_address: The address of the temporary web server the client should open. It's a tuple composed of the IP and the port. Defaults to ("localhost", 51562).
  • redirect_path: Redirect URI where Fief will redirect after a successful authentication. Defaults to /callback.
  • scope: Optional list of scopes to ask for. The client will always ask at least for openid and offline_access.
  • lang: Optional parameter to set the user locale on the authentication pages. Should be a valid RFC 3066 language identifier, like fr or pt-PT.
  • **extras_params: Optional dictionary containing specific parameters.

Example:

tokens, userinfo = auth.authorize()
def render_success_page(self) -> str:
270    def render_success_page(self) -> str:
271        """
272        Generate the HTML page that'll be shown to the user after a successful redirection.
273
274        By default, it just tells the user that it can go back to the CLI.
275
276        You can override this method if you want to customize this page.
277        """
278        return f"""
279        <html>
280            <head>
281                <link href="{self.client.base_url}/static/auth.css" rel="stylesheet">
282            </head>
283            <body class="antialiased">
284                <main>
285                    <div class="relative flex">
286                        <div class="w-full">
287                            <div class="min-h-screen h-full flex flex flex-col after:flex-1">
288                                <div class="flex-1"></div>
289                                <div class="w-full max-w-sm mx-auto px-4 py-8 text-center">
290                                    <h1 class="text-3xl text-accent font-bold mb-6">Done! You can go back to your terminal!</h1>
291                                </div>
292                            </div>
293                        </div>
294                    </div>
295                </main>
296                <script>
297                    window.addEventListener("DOMContentLoaded", () => {{
298                        setTimeout(() => {{
299                            window.close();
300                        }}, 5000);
301                    }});
302                </script>
303            </body>
304        </html>
305        """

Generate the HTML page that'll be shown to the user after a successful redirection.

By default, it just tells the user that it can go back to the CLI.

You can override this method if you want to customize this page.

def render_error_page(self, query_params: Dict[str, Any]) -> str:
307    def render_error_page(self, query_params: typing.Dict[str, typing.Any]) -> str:
308        """
309        Generate the HTML page that'll be shown to the user when something goes wrong during redirection.
310
311        You can override this method if you want to customize this page.
312        """
313        return f"""
314        <html>
315            <head>
316                <link href="{self.client.base_url}/static/auth.css" rel="stylesheet">
317            </head>
318            <body class="antialiased">
319                <main>
320                    <div class="relative flex">
321                        <div class="w-full">
322                            <div class="min-h-screen h-full flex flex flex-col after:flex-1">
323                                <div class="flex-1"></div>
324                                <div class="w-full max-w-sm mx-auto px-4 py-8 text-center">
325                                    <h1 class="text-3xl text-accent font-bold mb-6">Something went wrong! You're not authenticated.</h1>
326                                    <p>Error detail: {json.dumps(query_params)}</p>
327                                </div>
328                            </div>
329                        </div>
330                    </div>
331                </main>
332            </body>
333        </html>
334        """

Generate the HTML page that'll be shown to the user when something goes wrong during redirection.

You can override this method if you want to customize this page.

class FiefAuthError(builtins.Exception):
25class FiefAuthError(Exception):
26    """
27    Base error for FiefAuth integration.
28    """

Base error for FiefAuth integration.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
args
class FiefAuthNotAuthenticatedError(FiefAuthError):
31class FiefAuthNotAuthenticatedError(FiefAuthError):
32    """
33    The user is not authenticated.
34    """
35
36    pass

The user is not authenticated.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
args
class FiefAuthAuthorizationCodeMissingError(FiefAuthError):
39class FiefAuthAuthorizationCodeMissingError(FiefAuthError):
40    """
41    The authorization code was not found in the redirection URL.
42    """
43
44    pass

The authorization code was not found in the redirection URL.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
args
class FiefAuthRefreshTokenMissingError(FiefAuthError):
47class FiefAuthRefreshTokenMissingError(FiefAuthError):
48    """
49    The refresh token is missing in the saved credentials.
50    """
51
52    pass

The refresh token is missing in the saved credentials.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
args