示例#1
0
class JWTSecurityConfigurationEditForm(AdminEditForm):
    """JWT security configuration edit form"""

    title = _("Security manager")
    legend = _("JWT configuration")

    fields = Fields(IJWTSecurityConfiguration).select('access_token_name', 'refresh_token_name')

    def get_content(self):
        return IJWTSecurityConfiguration(self.context)

    def apply_changes(self, data):
        configuration = self.get_content()
        old_region = configuration.selected_cache
        changes = super(JWTSecurityConfigurationEditForm, self).apply_changes(data)
        if changes and (old_region is not None):
            clear_cache(JWT_PROXY_CACHE_NAME, old_region, JWT_PROXY_TOKENS_NAMESPACE)
        return changes
示例#2
0
class JWTConfigurationHeader(AlertMessage):
    """JWT configuration header"""

    status = 'info'

    _message = _("JWT authentication module \"local mode\" allows to generate, check and refresh "
                 "tokens locally.\n"
                 "You can choose to use a simple secret key to encrypt your tokens, or to "
                 "use a private and public keys pair (which can to be used to share tokens "
                 "between two applications).")
示例#3
0
class JWTProxyConfigurationHeader(AlertMessage):
    """JWT proxy configuration header"""

    status = 'info'

    _message = _("JWT authentication module \"proxy mode\" relies on another authentication "
                 "authority (which can be another application using this JWT package) to "
                 "generate, check and refresh tokens. This authority can be used to share "
                 "access tokens between different applications.\n"
                 "You can cache tokens to reduce the number of requests which will be forwarded "
                 "to the authentication authority.")
示例#4
0
class JWTConfigurationKeyAlert(AlertMessage):
    """JWT configuration keys alert"""

    status = 'info'
    css_class = 'mb-1 p-2'

    _message = _("""You can use the `openssl` command to generate your keys:

    openssl genpkey -algorithm RSA -out private-key.pem
    openssl rsa -pubout -in private-key.pem -out public-key.pem
""")
    message_renderer = 'markdown'
示例#5
0
 def check_configuration(self):
     """Check for JWT configuration"""
     if self.local_mode and self.proxy_mode:
         raise Invalid(_("You can't enable both local and proxy modes"))
     if self.local_mode:
         if not self.algorithm:
             raise Invalid(
                 _("You must choose an algorithm to enable JWT authentication"
                   ))
         if self.algorithm.startswith('HS'):  # pylint: disable=no-member
             if not self.secret:
                 raise Invalid(
                     _("You must define JWT secret to use HS256 algorithm"))
         elif self.algorithm.startswith('RS'):  # pylint: disable=no-member
             if not (self.private_key and self.public_key):
                 raise Invalid(
                     _("You must define a private and a public key to use "
                       "RS256 algorithm"))
     if self.proxy_mode:
         if not self.authority:
             raise Invalid(
                 _("You must define authentication authority to use proxy mode"
                   ))
         if self.use_cache and not self.selected_cache:
             raise Invalid(
                 _("You must choose a cache to enable tokens caching"))
示例#6
0
def get_jwt_token(request):
    """REST login endpoint for JWT authentication"""
    # check security manager utility
    sm = query_utility(ISecurityManager)  # pylint: disable=invalid-name
    if sm is None:
        raise HTTPServiceUnavailable()
    configuration = IJWTSecurityConfiguration(sm)
    if not configuration.enabled:
        raise HTTPServiceUnavailable()
    # check request params
    params = request.params if TEST_MODE else request.validated
    login = params.get('login')
    if not login:
        raise HTTPBadRequest()
    credentials = Credentials('jwt', id=login, **params)
    # use remote authentication authority
    if configuration.proxy_mode:
        handler = IJWTProxyHandler(sm, None)
        if handler is not None:
            status_code, tokens = handler.get_tokens(request, credentials)  # pylint: disable=assignment-from-no-return
            request.response.status_code = status_code
            return tokens
    # authenticate principal in security manager
    principal_id = sm.authenticate(credentials, request)
    if principal_id is not None:
        custom_claims = params.get('claims', {})
        request.response.cache_expires(configuration.refresh_expiration)
        return {
            'status':
            'success',
            configuration.access_token_name:
            create_jwt_token(request,
                             principal_id,
                             expiration=configuration.access_expiration,
                             obj=ACCESS_OBJECT,
                             **custom_claims),
            configuration.refresh_token_name:
            create_jwt_token(request,
                             principal_id,
                             expiration=configuration.refresh_expiration,
                             obj=REFRESH_OBJECT)
        }
    request.response.status_code = HTTPUnauthorized.code
    return {
        'status': 'error',
        'message': request.localizer.translate(_("Invalid credentials!"))
    }
示例#7
0
class ClaimsSchema(ClaimsObjectSchema):
    """Token claims schema"""
    sub = SchemaNode(String(), title=_("Principal ID"))
    iat = SchemaNode(Int(), title=_("Token issue timestamp, in seconds"))
    exp = SchemaNode(Int(), title=_("Token expiration timestamp, in seconds"))
示例#8
0
class ClaimsObjectSchema(MappingSchema):
    """Claims getter schema"""
    obj = SchemaNode(String(),
                     title=_("Token object"),
                     validator=OneOf((ACCESS_OBJECT, REFRESH_OBJECT)),
                     missing=drop)
示例#9
0
class TokensSchema(StatusSchema):
    """Tokens response schema"""
    accessToken = SchemaNode(String(), title=_("Access token"))
    refreshToken = SchemaNode(String(), title=_("Refresh token"))
示例#10
0
class StatusSchema(MappingSchema):
    """Base status response schema"""
    status = SchemaNode(String(),
                        title=_("Response status"),
                        validator=OneOf(('success', 'error')))
示例#11
0
class LoginSchema(ClaimsSetterSchema):
    """Login schema"""
    login = SchemaNode(String(), title=_("Login"))
    password = SchemaNode(String(), title=_("Password"))
示例#12
0
class ErrorSchema(MappingSchema):
    """Base error schema"""
    status = SchemaNode(String(), title=_("Response status"))
    message = SchemaNode(String(), title=_("Error message"), missing=drop)
示例#13
0
    """Claims getter schema"""
    obj = SchemaNode(String(),
                     title=_("Token object"),
                     validator=OneOf((ACCESS_OBJECT, REFRESH_OBJECT)),
                     missing=drop)


class ClaimsSchema(ClaimsObjectSchema):
    """Token claims schema"""
    sub = SchemaNode(String(), title=_("Principal ID"))
    iat = SchemaNode(Int(), title=_("Token issue timestamp, in seconds"))
    exp = SchemaNode(Int(), title=_("Token expiration timestamp, in seconds"))


jwt_responses = {
    HTTPOk.code: TokensSchema(description=_("Tokens properties")),
    HTTPAccepted.code: StatusSchema(description=_("Token accepted")),
    HTTPNotFound.code: ErrorSchema(description=_("Page not found")),
    HTTPUnauthorized.code: ErrorSchema(description=_("Unauthorized")),
    HTTPForbidden.code: ErrorSchema(description=_("Forbidden access")),
    HTTPBadRequest.code: ErrorSchema(description=_("Missing arguments")),
    HTTPServiceUnavailable.code:
    ErrorSchema(description=_("Service unavailable"))
}

if TEST_MODE:
    service_params = {}
else:
    service_params = {'response_schemas': jwt_responses}

jwt_token = Service(name=REST_TOKEN_ROUTE,
示例#14
0
class JWTSecurityConfiguration(NavigationMenuItem):
    """JWT security configuration menu"""

    label = _("JWT configuration")
    href = '#jwt-security-configuration.html'
示例#15
0
class IJWTSecurityConfiguration(Interface):
    """Security manager configuration interface for JWT"""

    access_token_name = TextLine(title=_("Access token attribute"),
                                 description=_(
                                     "Name of the JSON attribute containing "
                                     "access token returned by REST APIs"),
                                 required=False,
                                 default=ACCESS_TOKEN_NAME)

    refresh_token_name = TextLine(title=_("Refresh token attribute"),
                                  description=_(
                                      "Name of the JSON attribute containing "
                                      "refresh token returned by REST APIs"),
                                  required=False,
                                  default=REFRESH_TOKEN_NAME)

    enabled = Attribute("Enabled configuration?")

    local_mode = Bool(
        title=_("Enable JWT direct authentication?"),
        description=_("Enable direct login via JWT authentication"),
        required=False,
        default=False)

    algorithm = Choice(
        title=_("JWT encoding algorithm"),
        description=_(
            "HS* protocols are using the secret, while RS* protocols "
            "are using RSA keys"),
        required=False,
        values=('RS256', 'RS512', 'HS256', 'HS512'),
        default='RS512')

    secret = TextLine(
        title=_("JWT secret"),
        description=_("This secret is required when using HS* encryption"),
        required=False)

    private_key = Text(
        title=_("JWT private key"),
        description=_("The secret key is required when using RS* algorithm"),
        required=False)

    public_key = Text(
        title=_("JWT public key"),
        description=_("The public key is required when using RS* algorithm"),
        required=False)

    access_expiration = Int(
        title=_("Access token lifetime"),
        description=_("JWT access token lifetime, in seconds"),
        required=False,
        default=60 * 60)

    refresh_expiration = Int(
        title=_("Refresh token lifetime"),
        description=_("JWT refresh token lifetime, in seconds"),
        required=False,
        default=60 * 60 * 24 * 7)

    proxy_mode = Bool(
        title=_("Enable JWT proxy authentication?"),
        description=_("If this option is enabled, tokens management requests "
                      "will be forwarded to another authentication authority"),
        required=False,
        default=False)

    authority = TextLine(
        title=_("Authentication authority"),
        description=_("Base URL (protocol and hostname) of the authentication "
                      "authority to which tokens management requests will be "
                      "forwarded"),
        required=False)

    get_token_service = HTTPMethodField(
        title=_("Token getter service"),
        description=_("REST HTTP service used to get a new token"),
        required=False,
        default=('POST', '/api/auth/jwt/token'))

    proxy_access_token_name = TextLine(
        title=_("Access token attribute"),
        description=_("Name of the JSON attribute returned by "
                      "REST API containing access tokens"),
        required=False,
        default=ACCESS_TOKEN_NAME)

    proxy_refresh_token_name = TextLine(
        title=_("Refresh token attribute"),
        description=_("Name of the JSON attribute returned by "
                      "REST API containing refresh tokens"),
        required=False,
        default=REFRESH_TOKEN_NAME)

    get_claims_service = HTTPMethodField(
        title=_("Token claims getter"),
        description=_("REST HTTP service used to extract claims "
                      "from provided authorization token"),
        required=False,
        default=('GET', '/api/auth/jwt/token'))

    refresh_token_service = HTTPMethodField(
        title=_("Token refresh service"),
        description=_("REST HTTP service used to get a new "
                      "access token with a refresh token"),
        required=False,
        default=('PATCH', '/api/auth/jwt/token'))

    verify_token_service = HTTPMethodField(
        title=_("Token verify service"),
        description=_("REST HTTP service used to check "
                      "validity of an existing token"),
        required=False,
        default=('POST', '/api/auth/jwt/verify'))

    verify_ssl = Bool(
        title=_("Verify SSL?"),
        description=_("If 'no', SSL certificates will not be verified"),
        required=False,
        default=True)

    use_cache = Bool(
        title=_("Use verified tokens cache?"),
        description=_(
            "If selected, this option allows to store credentials in a "
            "local cache from which they can be reused"),
        required=False,
        default=True)

    selected_cache = Choice(
        title=_("Selected tokens cache"),
        description=_("Beaker cache selected to store validated tokens"),
        required=False,
        vocabulary=BEAKER_CACHES_VOCABULARY,
        default='default')

    @invariant
    def check_configuration(self):
        """Check for JWT configuration"""
        if self.local_mode and self.proxy_mode:
            raise Invalid(_("You can't enable both local and proxy modes"))
        if self.local_mode:
            if not self.algorithm:
                raise Invalid(
                    _("You must choose an algorithm to enable JWT authentication"
                      ))
            if self.algorithm.startswith('HS'):  # pylint: disable=no-member
                if not self.secret:
                    raise Invalid(
                        _("You must define JWT secret to use HS256 algorithm"))
            elif self.algorithm.startswith('RS'):  # pylint: disable=no-member
                if not (self.private_key and self.public_key):
                    raise Invalid(
                        _("You must define a private and a public key to use "
                          "RS256 algorithm"))
        if self.proxy_mode:
            if not self.authority:
                raise Invalid(
                    _("You must define authentication authority to use proxy mode"
                      ))
            if self.use_cache and not self.selected_cache:
                raise Invalid(
                    _("You must choose a cache to enable tokens caching"))
示例#16
0
class JWTAuthenticationPlugin(metaclass=ClassPropertyType):
    """JWT authentication plugin"""

    prefix = 'jwt'
    title = _("JWT authentication")

    audience = None
    leeway = 0
    callback = None
    json_encoder = None

    @classproperty
    def http_header(cls):  # pylint: disable=no-self-argument,no-self-use
        """HTTP header setting"""
        return get_current_registry().settings.get('pyams.jwt.http_header',
                                                   'Authorization')

    @classproperty
    def auth_type(cls):  # pylint: disable=no-self-argument,no-self-use
        """HTTP authentication type setting"""
        return get_current_registry().settings.get('pyams.jwt.auth_type',
                                                   'Bearer')

    @property
    def configuration(self):  # pylint: disable=no-self-use
        """JWT configuration getter"""
        try:
            manager = query_utility(ISecurityManager)
            if manager is not None:
                return IJWTSecurityConfiguration(manager)
        except ConnectionStateError:
            return None
        return None

    @property
    def enabled(self):
        """Check if JWT authentication is enabled in security manager"""
        configuration = self.configuration
        # pylint: disable=no-member
        try:
            return configuration.enabled if (configuration
                                             is not None) else False
        except ConnectionStateError:
            return False

    @property
    def expiration(self):
        """Get default security manager expiration"""
        configuration = self.configuration
        # pylint: disable=no-member
        return configuration.access_expiration if configuration is not None else None

    def create_token(self,
                     principal,
                     expiration=None,
                     audience=None,
                     **claims):
        """Create JWT token"""
        if not self.enabled:
            return None
        configuration = self.configuration
        payload = {}
        payload.update(claims)
        payload['sub'] = principal
        payload['iat'] = iat = datetime.utcnow()
        expiration = expiration or self.expiration
        if expiration:
            if not isinstance(expiration, timedelta):
                expiration = timedelta(seconds=expiration)
            payload['exp'] = iat + expiration
        audience = audience or self.audience
        if audience:
            payload['aud'] = audience
        # pylint: disable=no-member
        algorithm = configuration.algorithm if configuration is not None else 'RS512'
        if algorithm.startswith('HS'):
            # pylint: disable=no-member
            key = configuration.secret if configuration is not None else None
        else:  # RS256
            # pylint: disable=no-member
            key = configuration.private_key if configuration is not None else None
        token = jwt.encode(payload,
                           key,
                           algorithm=algorithm,
                           json_encoder=self.json_encoder)
        if not isinstance(token, str):
            token = token.decode('ascii')
        return token

    def _get_claims(self, request, obj=None):  # pylint: disable=too-many-return-statements
        """Get JWT claims"""
        if self.http_header == 'Authorization':  # pylint: disable=comparison-with-callable
            try:
                if request.authorization is None:
                    return {}
            except (ValueError,
                    AttributeError):  # invalid authorization header
                return {}
            (auth_type, token) = request.authorization
            if auth_type != self.auth_type:  # pylint: disable=comparison-with-callable
                return {}
        else:
            token = request.headers.get(self.http_header)
        if not token:
            return {}
        try:
            configuration = self.configuration
            # pylint: disable=no-member
            algorithm = configuration.algorithm if configuration is not None else 'RS512'
            if algorithm.startswith('HS'):
                # pylint: disable=no-member
                key = configuration.secret if configuration is not None else None
            else:  # RS256/RS512
                # pylint: disable=no-member
                key = configuration.public_key if configuration is not None else None
            claims = jwt.decode(token,
                                key,
                                algorithms=[algorithm],
                                leeway=self.leeway,
                                audience=self.audience)
            if obj and obj != claims.get('obj'):
                raise InvalidTokenError('Bad token object!')
            return claims
        except InvalidTokenError as exc:
            LOGGER.warning('Invalid JWT token from %s: %s',
                           getattr(request, 'remote_addr', '--'), exc)
            return {}

    def get_claims(self, request, obj=None):  # pylint: disable=too-many-return-statements
        """Get JWT claims"""
        configuration = self.configuration
        if configuration is None:
            return {}
        if configuration.proxy_mode:
            handler = IJWTProxyHandler(self)
            if handler is not None:
                _status_code, claims = handler.get_claims(request, obj)  # pylint: disable=assignment-from-no-return
                return claims
        elif configuration.local_mode:
            return self._get_claims(request, obj)
        return {}

    def extract_credentials(self, request, **kwargs):  # pylint: disable=unused-argument
        """Extract principal ID from given request"""
        claims = self.get_claims(request, obj=ACCESS_OBJECT)
        if claims:
            return Credentials(self.prefix,
                               claims.get('sub'),
                               login=claims.get('login')) if claims else None
        return None

    def authenticate(self, credentials, request):  # pylint: disable=unused-argument
        """Authenticate JWT token"""
        claims = self.get_claims(request, obj=ACCESS_OBJECT)
        return claims.get('sub') if claims else None

    def unauthenticated_userid(self, request):
        """Get unauthenticated user ID"""
        claims = self.get_claims(request, obj=ACCESS_OBJECT)
        return claims.get('sub') if claims else None
示例#17
0
class ClaimsSetterSchema(MappingSchema):
    """Claims setter schema"""
    claims = SchemaNode(PropertiesMapping(),
                        title=_("Custom claims"),
                        missing=drop)