def __init__(self, provider: SAMLProvider, request: HttpRequest, auth_n_request: AuthNRequest): self.provider = provider self.http_request = request self.auth_n_request = auth_n_request self._issue_instant = get_time_string() self._assertion_id = get_random_id() self._valid_not_before = get_time_string( timedelta_from_string(self.provider.assertion_valid_not_before)) self._valid_not_on_or_after = get_time_string( timedelta_from_string( self.provider.assertion_valid_not_on_or_after))
def get(self, request: HttpRequest) -> HttpResponse: """Attach the currently pending user to the current session""" if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context: message = _("No Pending user to login.") messages.error(request, message) LOGGER.debug(message) return self.executor.stage_invalid() backend = self.executor.plan.context.get( PLAN_CONTEXT_AUTHENTICATION_BACKEND, DEFAULT_BACKEND ) login( self.request, self.executor.plan.context[PLAN_CONTEXT_PENDING_USER], backend=backend, ) delta = timedelta_from_string(self.executor.current_stage.session_duration) if delta.seconds == 0: self.request.session.set_expiry(0) else: self.request.session.set_expiry(delta) LOGGER.debug( "Logged in", user=self.executor.plan.context[PLAN_CONTEXT_PENDING_USER], flow_slug=self.executor.flow.slug, session_duration=self.executor.current_stage.session_duration, ) messages.success(self.request, _("Successfully logged in!")) return self.executor.stage_ok()
def get(self, request: HttpRequest) -> HttpResponse: """Attach the currently pending user to the current session""" if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context: message = _("No Pending user to login.") messages.error(request, message) LOGGER.debug(message) return self.executor.stage_invalid() backend = self.executor.plan.context.get( PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_INBUILT) user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] if not user.is_active: LOGGER.warning("User is not active, login will not work.") login( self.request, user, backend=backend, ) delta = timedelta_from_string( self.executor.current_stage.session_duration) if delta.total_seconds() == 0: self.request.session.set_expiry(0) else: self.request.session.set_expiry(delta) LOGGER.debug( "Logged in", backend=backend, user=user, flow_slug=self.executor.flow.slug, session_duration=self.executor.current_stage.session_duration, ) self.request.session[USER_LOGIN_AUTHENTICATED] = True messages.success(self.request, _("Successfully logged in!")) return self.executor.stage_ok()
def clean_temporary_users(self: MonitoredTask): """Remove temporary users created by SAML Sources""" _now = now() messages = [] deleted_users = 0 for user in User.objects.filter(attributes__saml__isnull=False): sources = SAMLSource.objects.filter( pk=user.attributes.get("saml", {}).get("source", "")) if not sources.exists(): LOGGER.warning( "User has an invalid SAML Source and won't be deleted!", user=user) messages.append( f"User {user} has an invalid SAML Source and won't be deleted!" ) continue source = sources.first() source_delta = timedelta_from_string( source.temporary_user_delete_after) if _now - user.last_login >= source_delta: LOGGER.debug("User is expired and will be deleted.", user=user, delta=source_delta) # TODO: Check if user is signed in anywhere? user.delete() deleted_users += 1 messages.append(f"Successfully deleted {deleted_users} users.") self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
def test_event_retention(self): """Test tenant's event retention""" tenant = Tenant.objects.create( domain="foo", default=True, branding_title="custom", event_retention="weeks=3", ) factory = RequestFactory() request = factory.get("/") request.tenant = tenant event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request) self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day) self.assertEqual( event.expires.month, (event.created + timedelta_from_string("weeks=3")).month, ) self.assertEqual( event.expires.year, (event.created + timedelta_from_string("weeks=3")).year )
def create_refresh_token(self, user: User, scope: list[str], request: HttpRequest) -> "RefreshToken": """Create and populate a RefreshToken object.""" token = RefreshToken( user=user, provider=self, refresh_token=uuid4().hex, expires=timezone.now() + timedelta_from_string(self.token_validity), scope=scope, ) token.access_token = token.create_access_token(user, request) return token
def create_id_token(self, user: User, request: HttpRequest) -> IDToken: """Creates the id_token. See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken""" sub = "" if self.provider.sub_mode == SubModes.HASHED_USER_ID: sub = sha256(f"{user.id}-{settings.SECRET_KEY}".encode( "ascii")).hexdigest() elif self.provider.sub_mode == SubModes.USER_EMAIL: sub = user.email elif self.provider.sub_mode == SubModes.USER_USERNAME: sub = user.username elif self.provider.sub_mode == SubModes.USER_UPN: sub = user.attributes["upn"] else: raise ValueError((f"Provider {self.provider} has invalid sub_mode " f"selected: {self.provider.sub_mode}")) # Convert datetimes into timestamps. now = int(time.time()) iat_time = now exp_time = int( now + timedelta_from_string(self.provider.token_validity).seconds) # We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time auth_events = Event.objects.filter( action=EventAction.LOGIN, user=get_user(user)).order_by("-created") # Fallback in case we can't find any login events auth_time = datetime.now() if auth_events.exists(): auth_time = auth_events.first().created auth_time = int(dateformat.format(auth_time, "U")) token = IDToken( iss=self.provider.get_issuer(request), sub=sub, aud=self.provider.client_id, exp=exp_time, iat=iat_time, auth_time=auth_time, ) # Include (or not) user standard claims in the id_token. if self.provider.include_claims_in_id_token: from authentik.providers.oauth2.views.userinfo import UserInfoView user_info = UserInfoView() user_info.request = request claims = user_info.get_claims(self) token.claims = claims return token
def create_refresh_response(self) -> dict[str, Any]: """See https://tools.ietf.org/html/rfc6749#section-6""" unauthorized_scopes = set(self.params.scope) - set( self.params.refresh_token.scope) if unauthorized_scopes: raise TokenError("invalid_scope") provider: OAuth2Provider = self.params.refresh_token.provider refresh_token: RefreshToken = provider.create_refresh_token( user=self.params.refresh_token.user, scope=self.params.scope, request=self.request, ) # If the Token has an id_token it's an Authentication request. if self.params.refresh_token.id_token: refresh_token.id_token = refresh_token.create_id_token( user=self.params.refresh_token.user, request=self.request, ) refresh_token.id_token.at_hash = refresh_token.at_hash # Store the refresh_token. refresh_token.save() # Mark old token as revoked self.params.refresh_token.revoked = True self.params.refresh_token.save() return { "access_token": refresh_token.access_token, "refresh_token": refresh_token.refresh_token, "token_type": "bearer", "expires_in": int( timedelta_from_string( refresh_token.provider.token_validity).total_seconds()), "id_token": self.params.provider.encode(refresh_token.id_token.to_dict()), }
def from_http(self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None) -> "Event": """Add data from a Django-HttpRequest, allowing the creation of Events independently from requests. `user` arguments optionally overrides user from requests.""" if request: self.context["http_request"] = { "path": request.path, "method": request.method, "args": QueryDict(request.META.get("QUERY_STRING", "")), } if hasattr(request, "tenant"): tenant: Tenant = request.tenant # Because self.created only gets set on save, we can't use it's value here # hence we set self.created to now and then use it self.created = now() self.expires = self.created + timedelta_from_string( tenant.event_retention) self.tenant = sanitize_dict(model_to_dict(tenant)) if hasattr(request, "user"): original_user = None if hasattr(request, "session"): original_user = request.session.get( SESSION_IMPERSONATE_ORIGINAL_USER, None) self.user = get_user(request.user, original_user) if user: self.user = get_user(user) # Check if we're currently impersonating, and add that user if hasattr(request, "session"): if SESSION_IMPERSONATE_ORIGINAL_USER in request.session: self.user = get_user( request.session[SESSION_IMPERSONATE_ORIGINAL_USER]) self.user["on_behalf_of"] = get_user( request.session[SESSION_IMPERSONATE_USER]) # User 255.255.255.255 as fallback if IP cannot be determined self.client_ip = get_client_ip(request) # Apply GeoIP Data, when enabled self.with_geoip() # If there's no app set, we get it from the requests too if not self.app: self.app = Event._get_app_from_request(request) self.save() return self
def create_code(self, request: HttpRequest) -> AuthorizationCode: """Create an AuthorizationCode object for the request""" code = AuthorizationCode() code.user = request.user code.provider = self.provider code.code = uuid4().hex if self.code_challenge and self.code_challenge_method: code.code_challenge = self.code_challenge code.code_challenge_method = self.code_challenge_method code.expires_at = timezone.now() + timedelta_from_string( self.provider.access_code_validity) code.scope = self.scope code.nonce = self.nonce code.is_open_id = SCOPE_OPENID in self.scope return code
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: current_stage: ConsentStage = self.executor.current_stage if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context: return self.executor.stage_ok() application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION] # Make this StageView work when injected, in which case `current_stage` is an instance # of the base class, and we don't save any consent, as it is assumed to be a one-time # prompt if not isinstance(current_stage, ConsentStage): return self.executor.stage_ok() # Since we only get here when no consent exists, we can create it without update if current_stage.mode == ConsentMode.PERMANENT: UserConsent.objects.create( user=self.request.user, application=application, expiring=False ) if current_stage.mode == ConsentMode.EXPIRING: UserConsent.objects.create( user=self.request.user, application=application, expires=now() + timedelta_from_string(current_stage.consent_expire_in), ) return self.executor.stage_ok()
def create_code_response(self) -> dict[str, Any]: """See https://tools.ietf.org/html/rfc6749#section-4.1""" refresh_token = self.params.authorization_code.provider.create_refresh_token( user=self.params.authorization_code.user, scope=self.params.authorization_code.scope, request=self.request, ) if self.params.authorization_code.is_open_id: id_token = refresh_token.create_id_token( user=self.params.authorization_code.user, request=self.request, ) id_token.nonce = self.params.authorization_code.nonce id_token.at_hash = refresh_token.at_hash refresh_token.id_token = id_token # Store the token. refresh_token.save() # We don't need to store the code anymore. self.params.authorization_code.delete() return { "access_token": refresh_token.access_token, "refresh_token": refresh_token.refresh_token, "token_type": "bearer", "expires_in": int( timedelta_from_string( self.params.provider.token_validity).total_seconds()), "id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()), }
def create_implicit_response(self, code: Optional[AuthorizationCode]) -> dict: """Create implicit response's URL Fragment dictionary""" query_fragment = {} token = self.provider.create_refresh_token( user=self.request.user, scope=self.params.scope, request=self.request, ) # Check if response_type must include access_token in the response. if self.params.response_type in [ ResponseTypes.ID_TOKEN_TOKEN, ResponseTypes.CODE_ID_TOKEN_TOKEN, ResponseTypes.ID_TOKEN, ResponseTypes.CODE_TOKEN, ]: query_fragment["access_token"] = token.access_token # We don't need id_token if it's an OAuth2 request. if SCOPE_OPENID in self.params.scope: id_token = token.create_id_token( user=self.request.user, request=self.request, ) id_token.nonce = self.params.nonce # Include at_hash when access_token is being returned. if "access_token" in query_fragment: id_token.at_hash = token.at_hash if self.params.response_type in [ ResponseTypes.CODE_ID_TOKEN, ResponseTypes.CODE_ID_TOKEN_TOKEN, ]: id_token.c_hash = code.c_hash # Check if response_type must include id_token in the response. if self.params.response_type in [ ResponseTypes.ID_TOKEN, ResponseTypes.ID_TOKEN_TOKEN, ResponseTypes.CODE_ID_TOKEN, ResponseTypes.CODE_ID_TOKEN_TOKEN, ]: query_fragment["id_token"] = self.provider.encode(id_token.to_dict()) token.id_token = id_token # Store the token. token.save() # Code parameter must be present if it's Hybrid Flow. if self.params.grant_type == GrantTypes.HYBRID: query_fragment["code"] = code.code query_fragment["token_type"] = "bearer" query_fragment["expires_in"] = timedelta_from_string( self.provider.token_validity ).seconds query_fragment["state"] = self.params.state if self.params.state else "" return query_fragment
def get_token_validity(self, obj: ProxyProvider) -> Optional[float]: """Get token validity as second count""" return timedelta_from_string(obj.token_validity).total_seconds()
def test_invalid(self): """Test invalid expression""" with self.assertRaises(ValueError): timedelta_from_string("foo") with self.assertRaises(ValueError): timedelta_from_string("bar=baz")
def test_valid(self): """Test valid expression""" expr = "hours=3;minutes=1" expected = timedelta(hours=3, minutes=1) self.assertEqual(timedelta_from_string(expr), expected)