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
[email protected]
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