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

Reply via email to