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