Hello,

+1 for upgrading the API to support OAuth2 / JWT Authenticating Realm

I have implemented custom realm to support OAuth2 login when using Shiro 1.5.2 
which support OAuth2 flow as well as REST API carrying JWT.

This project 
(https://github.com/zhangkaitao/shiro-example/blob/master/shiro-example-chapter17-client/src/main/)
 helped me to start the implementation.

After reading this thread, I thought I should share some of the requirements I 
have received while implementing it.

We should be able to configure the name of the header which is carrying the JWT 
token. For example, WSO2 allows it to configure this header and by default it 
is X-JWT-Assertion (Reference: 
https://docs.wso2.com/display/AM200/Passing+Enduser+Attributes+to+the+Backend+Using+JWT)

+ 1 for using replace method as in our scenario, the header value will not have 
this text.

bearerToken.getToken().replaceFirst("Bearer ", "")

We should have option to configure the JWT verification with JWKS.

As JWT carries more information about the subject, we should make it available 
on thread/session context so that business logic can get the JWT claims if 
required.

Please let me know if you need clarity. Happy to help!

Thanks,
Mahendran.


On 9/11/20, 2:06 AM, "Francois Papon" <francois.pa...@openobject.fr> wrote:

    NOTE: This message is from an EXTERNAL SENDER - be CAUTIOUS, particularly 
with links and attachments.
    Please contact Information Security team for suspicious content/activity.


    Hi,

    +1 for an upgrade of the api!

    I made a PoC with JWT in Shiro last year and I was using the cache to
    store the authorization information from the JWT in the principal during
    the authentication phase:

    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jws;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import java.security.KeyFactory;
    import java.security.PublicKey;
    import java.security.spec.X509EncodedKeySpec;
    import java.time.Instant;
    import java.util.*;
    import org.apache.shiro.authc.*;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.authz.Permission;
    import org.apache.shiro.authz.permission.WildcardPermission;
    import org.apache.shiro.codec.Hex;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.osgi.service.cm.ConfigurationException;
    import org.osgi.service.component.ComponentContext;
    import org.osgi.service.component.annotations.Activate;
    import org.osgi.service.component.annotations.Component;
    import org.osgi.service.component.annotations.Deactivate;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    @Component(immediate = true, name =
    "fr.openobject.labs.shiro.services.security", service = JwtRealm.class)
    public class JwtRealm extends AuthorizingRealm {

        private Logger logger = LoggerFactory.getLogger(JwtRealm.class);

        private PublicKey publicKey;
        private SignatureAlgorithm algorithm;

        public JwtRealm() {
            this.setAuthenticationTokenClass(BearerToken.class);
        }

        @Activate
        public void activate(ComponentContext componentContext) throws
    Exception {

            Dictionary<String, Object> properties =
    componentContext.getProperties();
            String hexPublicKey =
    String.class.cast(properties.get("security.publicKey"));
            String propAlgorithm =
    String.class.cast(properties.get("security.algorithm"));

            if (propAlgorithm != null && !propAlgorithm.equals("")) {
                this.algorithm = SignatureAlgorithm.forName(propAlgorithm);
            } else {
                logger.info("No signature algorithm found, using RS512...");
                this.algorithm = SignatureAlgorithm.RS512;
            }

            if (hexPublicKey == null || hexPublicKey.equals("")) {
                logger.info("Missing public key configuration!");
                throw new ConfigurationException("security.publicKey",
    "Missing public key");
            }

            X509EncodedKeySpec x509 = new
    X509EncodedKeySpec(Hex.decode(hexPublicKey));
            this.publicKey =

    KeyFactory.getInstance(this.algorithm.getFamilyName()).generatePublic(x509);
        }

        @Deactivate
        public void deactivate() {
            // do nothing
        }

        @Override
        protected AuthorizationInfo
    doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            // not implemented, we are using the cache in the authentication
    info method
            return null;
        }

        @Override
        protected AuthenticationInfo
    doGetAuthenticationInfo(AuthenticationToken authenticationToken)
                throws AuthenticationException {

            SimpleAccount account = null;
            BearerToken bearerToken =
    BearerToken.class.cast(authenticationToken);

            if (bearerToken != null) {
                try {
                    Jws<Claims> jws =
                            Jwts.parser()
                                    .setSigningKey(publicKey)

    .parseClaimsJws(bearerToken.getToken().replaceFirst("Bearer ", ""));

                    if (jws != null
                            && jws.getBody() != null
                            &&
    jws.getBody().getExpiration().after(Date.from(Instant.now()))) {

                        Set<String> roles = new
    HashSet<>(jws.getBody().get("roles", ArrayList.class));
                        List<String> permissions =
                                new
    ArrayList<>(jws.getBody().get("permissions", ArrayList.class));
                        Set<Permission> finalPermission = new HashSet<>();

                        permissions
                                .stream()
                                .forEach(perm -> finalPermission.add(new
    WildcardPermission(perm)));

                        String customer = jws.getBody().get("customer",
    String.class);
                        account =
                                new SimpleAccount(
                                        customer + ":" +
    jws.getBody().getSubject(),
                                        jws.getBody().getId(),
                                        "MYAPP",
                                        roles,
                                        finalPermission);
                        logger.debug(account.toString());

                        if (this.isAuthorizationCachingEnabled()) {
                            Object cacheAuthzKey =

    this.getAuthorizationCacheKey(account.getPrincipals());
                            if
    (Optional.ofNullable(cacheAuthzKey).isPresent()) {

    this.getAuthorizationCache().put(cacheAuthzKey, account);
                            }
                        }
                    }
                } catch (Exception exception) {
                    logger.warn(
                            "not a valid token! :: {} :: cause :: {}",
                            bearerToken.getToken().replaceFirst("Bearer ", ""),
                            exception.getMessage());
                }
            }
            return account;
        }

    }

    regards,

    François
    fpa...@apache.org

    Le 10/09/2020 à 22:21, Benjamin Marwell a écrit :
    > Hi everyone,
    >
    > I would like to adapt shiro to be able to read authentication and
    > authorization data from JWT tokens.
    > It is quite easy for authentication. Next to
    > UsernamePasswordToken.java, we could create an JwtToken.java class,
    > which holds Jwt Authorization Data (the signature) and Authentication
    > data (claims). Maybe by pulling in the MicroProfile JWT API [1], or
    > maybe just an abstraction of it.
    >
    > For the authorization, it gets a little more complicated.
    > At the moment we have this following method in the file 
AuthorizingRealm.java:
    >
    >     protected AuthorizationInfo
    > getAuthorizationInfo(PrincipalCollection principals);
    >
    > It takes a PrincipalCollection, because the user can be part of
    > multiple realms. However, the authorization data is (possibly!) stored
    > in the JWT, which is not available anymore. However, a simple API
    > change could make it available. I would like change it to:
    >
    >     protected AuthorizationInfo
    > getAuthorizationInfo(PrincipalCollection principals,
    > AuthenticationToken token);
    >
    > Of course, the implementations would still be able to pull even more
    > authentication data (e.g. additional roles not stored in the JWT) from
    > a database or other external source. However, currently there is no
    > non-hacky way to pull in the JwtToken in the AuthorizingRealm.java
    > class.
    >
    > --
    >
    > Parsing of such a token is also necessary. While MP-JWT is just an
    > API, one implementation must either be shipped or chosen by the user.
    > Or maybe shipped, and if the user wishes to use another
    > implementation, it can be excluded and the other dependency will be
    > pulled in.
    >
    > The API is simple [2] and allows easy migration of to those who do not
    > need inbuilt authentication.
    >
    > However, there are still several use cases for JWT in shiro:
    > * multi-server-readable authentication/authorization using a JWT. This
    > would make shiro-apps totally stateless without any shared state (e.g.
    > a shared session cache in a DB or via a memory grid).
    > * allowing multiple login methods (JWT and other realms, like LDAP) in
    > combination with the FirstSuccessfulStrategy.
    > * Using another JWT library, the application could create a single or
    > multiple JWTs itself after a user LDAP login to replace the cookie and
    > associated sessions. But better have an external (trusted) service to
    > issue tokens.
    >
    > Please let me know what you think.
    >
    > [1] 
https://www.eclipse.org/community/eclipse_newsletter/2017/september/article2.php
    >
    > [2] https://www.tomitribe.com/blog/microprofile-json-web-token-jwt/
    >
    > Regards,
    > Ben


NVOCC Services are provided by CEVA as agents for and on behalf of Pyramid 
Lines Limited trading as Pyramid Lines.
This e-mail message is intended for the above named recipient(s) only. It may 
contain confidential information that is privileged. If you are not the 
intended recipient, you are hereby notified that any dissemination, 
distribution or copying of this e-mail and any attachment(s) is strictly 
prohibited. If you have received this e-mail by error, please immediately 
notify the sender by replying to this e-mail and deleting the message including 
any attachment(s) from your system. Thank you in advance for your cooperation 
and assistance. Although the company has taken reasonable precautions to ensure 
no viruses are present in this email, the company cannot accept responsibility 
for any loss or damage arising from the use of this email or attachments.

Reply via email to