Skip to content

Commit

Permalink
Merge pull request #46 from djpugh/feature/auto-auth-for-me-token
Browse files Browse the repository at this point in the history
  • Loading branch information
djpugh authored Dec 20, 2020
2 parents d65f8ef + bec22bd commit 67d480e
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 31 deletions.
1 change: 1 addition & 0 deletions src/fastapi_aad_auth/_base/authenticators/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def process_login_request(self, request, force=False, redirect='/'):
"""Process the provider login request."""
self.logger.debug(f'Logging in - request url {request.url}')
auth_state = self._session_validator.get_state_from_session(request)
force = request.query_params.get('force', force)
if auth_state.is_authenticated() and not force:
self.logger.debug(f'Authenticated - redirecting {auth_state}')
response = self.redirect_if_authenticated(auth_state)
Expand Down
4 changes: 2 additions & 2 deletions src/fastapi_aad_auth/_base/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ def __init__(self, validators: List[Validator], authenticator: SessionAuthentica
def get_routes(self, noauth_redirect='/'):
"""Get the authenticator routes."""

async def login(request: Request):
async def login(request: Request, force: bool = False, redirect: str = '/'):
self.logger.debug(f'Logging in with {self.name} - request url {request.url}')
if self.enabled:
self.logger.debug(f'Auth {request.auth}')
return self.authenticator.process_login_request(request)
return self.authenticator.process_login_request(request, force=force)
else:
self.logger.debug('Auth not enabled')
return RedirectResponse(noauth_redirect)
Expand Down
25 changes: 18 additions & 7 deletions src/fastapi_aad_auth/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* ``user.html``: View the user's information and get an access token
"""
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union

from fastapi import Depends
from starlette.requests import Request
Expand Down Expand Up @@ -82,7 +82,7 @@ def _get_user(self, request: Request, **kwargs):
context['token_api_path'] = None # type: ignore
return self.user_templates.TemplateResponse(self.user_template_path.name, context)

def _get_token(self, request: Request, auth_state: AuthenticationState, scopes: Optional[List[str]] = None):
def _get_token(self, request: Request, auth_state: AuthenticationState, scopes: Optional[List[str]] = None, ajax: bool = False):
"""Return the access token for the user."""
if not isinstance(auth_state, AuthenticationState):
user = self.__get_user_from_request(request)
Expand All @@ -100,7 +100,7 @@ def _get_token(self, request: Request, auth_state: AuthenticationState, scopes:
else:
if any([u in request.headers['user-agent'] for u in ['Mozilla', 'Gecko', 'Trident', 'WebKit', 'Presto', 'Edge', 'Blink']]):
# If we have one provider, we can force the login, otherwise we need to request which login route
return self.__force_authenticate(request)
return self.__force_authenticate(request, ajax)
else:
return JSONResponse('Unable to access token as user has not authenticated via session')
redirect = '/me/token'
Expand Down Expand Up @@ -130,22 +130,33 @@ async def get_user(request: Request):
return self._get_user(request)

async def get_token(request: Request, auth_state: AuthenticationState = Depends(self._authenticator.auth_backend.requires_auth(allow_session=True)), scopes: Optional[List[str]] = None):
return self._get_token(request, auth_state, scopes)
ajax = request.query_params.get('ajax', False)
return self._get_token(request, auth_state, scopes, ajax)

routes += [Route(self.config.routing.user_path, endpoint=get_user, methods=['GET'], name='user'),
Route(f'{self.config.routing.user_path}/token', endpoint=get_token, methods=['GET'], name='get-token')]

return routes

def __force_authenticate(self, request: Request):
def __force_authenticate(self, request: Request, ajax: bool = False) -> Union[JSONResponse, RedirectResponse]:
# lets get the full redirect including any query parameters
redirect = urls.with_query_params(request.url.path, **request.query_params)
self.logger.debug(f'Request {request.url}')
self.logger.info(f'Forcing authentication with redirect = {redirect}')
if len(self._authenticator._providers) == 1:
return self._authenticator._providers[0].authenticator.process_login_request(request, force=True, redirect=redirect)
redirect_url = urls.with_query_params(self._authenticator._providers[0].login_url, redirect=redirect, force=True)
else:
return RedirectResponse(urls.with_query_params(self.config.routing.landing_path, redirect=redirect))
redirect_url = urls.with_query_params(self.config.routing.landing_path, redirect=redirect)
if ajax:
self.logger.debug(f'AJAX is true - handling {redirect_url}')
url = urls.parse_url(redirect_url)
query_params = urls.query_params(redirect_url)
query_params.pop('redirect', None)
self.logger.debug(f'url {url.path}, query_params {query_params}')
response = JSONResponse({'redirect': url.path, 'query_params': query_params}) # type: ignore
else:
response = RedirectResponse(redirect_url) # type: ignore
return response

def __get_access_token(self, user, scopes=None):
access_token = None
Expand Down
41 changes: 21 additions & 20 deletions src/fastapi_aad_auth/ui/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
{% if appname %}
<h1 class="h1">{{appname}}</h1>
{% endif %}

</p>
</main>
</div>
Expand All @@ -35,22 +35,22 @@ <h2>Current User</h2>
<th class="col-2" scope="row">Username</th>
<td class="col-10">{{user.username}}</td>
</tr>

<tr class="d-flex">
<th class="col-2" scope="row">Name</th>
<td class="col-10">{{user.name}}</td>
</tr>

<tr class="d-flex">
<th class="col-2" scope="row">Email</th>
<td class="col-10">{{user.email}}</td>
</tr>

<tr class="d-flex">
<th class="col-2" scope="row">Roles</th>
<td class="col-10">{% if user.roles %}{{user.roles|join(", ")}}{% endif %}</td>
</tr>

<tr class="d-flex">
<th class="col-2" scope="row">Groups</th>
<td class="col-10">{% if user.groups %}{{user.groups|join(", ")}}{% endif %}</td>
Expand All @@ -59,7 +59,7 @@ <h2>Current User</h2>
<th class="col-2" scope="row">Permissions</th>
<td class="col-10"><i>{% if user.permissions %}{{user.permissions | join("</i>, <i>") | safe}}{% endif %}</i></td>
</tr>

<tr class="d-flex">
<th class="col-2" scope="row">Bearer Token</th>
<td class="col-10">
Expand All @@ -74,9 +74,9 @@ <h2>Current User</h2>
<span id="expires"></span>
</td>
</tr>



</tbody>
</table>
</div>
Expand All @@ -97,18 +97,19 @@ <h2>Current User</h2>
$(document).ready(function(){
$('#tokenText').val('*********');
$('#expires').text("");
$("#loadToken").click(function(e){
$("#loadToken").click(function(e){
if (!e.currentTarget.classList.contains("active")){
$.ajax("{{token_api_path}}").done(function(data){
$('#tokenText').val(data['access_token']);
var expires = new Date();
console.log(expires);
console.log(expires);

expires.setSeconds(expires.getSeconds()+data['expires_in']);
console.log(expires);
console.log(expires.toISOString());
$('#expires').text("Expires: "+expires.toString())
$.ajax("{{token_api_path}}?ajax=True").done(function(data){
if (data.redirect) {
data.query_params.redirect = "{{ request.path }}"
login_url = data.redirect+"?"+$.param(data.query_params);
window.location.replace(login_url);
} else {
$('#tokenText').val(data['access_token']);
var expires = new Date();
expires.setSeconds(expires.getSeconds()+data['expires_in']);
$('#expires').text("Expires: "+expires.toString())
}
});
e.currentTarget.className += " active"}
else{
Expand Down
20 changes: 18 additions & 2 deletions src/fastapi_aad_auth/utilities/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""URL utilities."""
import logging

from starlette.datastructures import URL
from starlette.datastructures import parse_qsl, URL


logger = logging.getLogger(__name__)
Expand All @@ -24,10 +24,26 @@ def append(base_url, *args):
return url


def parse_url(url):
"""Parse the URL."""
return URL(url)


def query_params(url):
"""Extract the query parameters from a url."""
logger.debug(f'Parsing Query Params from {url}')
url = parse_url(url)
query = url.query
logger.debug(f'Extracted Query {query}')
parsed_params = parse_qsl(query)
logger.debug(f'Extracted Query Params {parsed_params}')
return dict(parsed_params)


def with_query_params(url, **query_params):
"""Add query parameters to a url."""
logger.debug(f'Adding {query_params} to {url}')
parsed_url = URL(url)
parsed_url = parse_url(url)
logger.debug(f'Existing query params {parsed_url.query}')
new_url = parsed_url.include_query_params(**query_params)
logger.debug(f'Updated query params {new_url.query}')
Expand Down

0 comments on commit 67d480e

Please sign in to comment.