Using Dropwizard 1.2.0 and Dropwizard JWT library, I am trying to create json web tokens from an API endpoint called /token
This endpoint requires the client to pass a username and password, using Basic Authentication method. If successful the response will contain a JSON web token.
Principal
public class ShepherdAuth implements JwtCookiePrincipal {
private String name;
private Set<String> roles;
public ShepherdAuth(String name, Set<String> roles) {
this.name = checkNotNull(name, "User name is required");
this.roles = checkNotNull(roles, "Roles are required");
}
#Override
public boolean isPersistent() {
return false;
}
#Override
public boolean isInRole(final String s) {
return false;
}
#Override
public String getName() {
return this.name;
}
#Override
public boolean implies(Subject subject) {
return false;
}
public Set<String> getRoles() {
return roles;
}
}
Authenticator
public class ShepherdAuthenticator implements Authenticator<BasicCredentials, ShepherdAuth> {
private static final Map<String, Set<String>> VALID_USERS = ImmutableMap.of(
"guest", ImmutableSet.of(),
"shepherd", ImmutableSet.of("SHEPHERD"),
"admin", ImmutableSet.of("ADMIN", "SHEPHERD")
);
#Override
public Optional<ShepherdAuth> authenticate(BasicCredentials credentials) throws AuthenticationException {
if (VALID_USERS.containsKey(credentials.getUsername()) && "password".equals(credentials.getPassword())) {
return Optional.of(new ShepherdAuth(credentials.getUsername(), VALID_USERS.get(credentials.getUsername())));
}
return Optional.empty();
}
}
Resource / Controller
public class ShepherdController implements ShepherdApi {
public ShepherdController() {
}
#PermitAll
#GET
#Path("/token")
public ShepherdAuth auth(#Auth final BasicCredentials user) {
return new ShepherdAuth(user.getUsername(),
ImmutableSet.of("SHEPHERD"));
}
App / Config
#Override
public void run(final ShepherdServiceConfiguration configuration,
final Environment environment) {
final ShepherdController shepherdController = new ShepherdController();
// app authentication
environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<ShepherdAuth>()
.setAuthenticator(new ShepherdAuthenticator())
.setAuthorizer(new ShepherdAuthorizer())
.setRealm(configuration.getName())
.buildAuthFilter()));
When I try to make a request to /shepherd/token I do not get a prompt for basic auth, instead I get a HTTP 401 response with
Credentials are required to access this resource.
How do I get the controller to prompt for username and password and generate a JWT on success?
I implemented JWT tokens in my project by using https://github.com/jwtk/jjwt but the solution will easily be applied to another library. The trick is to use different authenticators.
This answer is not suited for Dropwizard JWT Library but does the fine job of providing JWT for Dropwizard :)
First, the application:
environment.jersey().register(new TokenResource(configuration.getJwsSecretKey()));
environment.jersey().register(new HelloResource());
environment.jersey().register(RolesAllowedDynamicFeature.class);
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
environment.jersey()
.register(
new AuthDynamicFeature(
new ChainedAuthFilter<>(
Arrays
.asList(
new JWTCredentialAuthFilter.Builder<User>()
.setAuthenticator(
new JWTAuthenticator(configuration.getJwsSecretKey()))
.setPrefix("Bearer").setAuthorizer(new UserAuthorizer())
.buildAuthFilter(),
new JWTDefaultCredentialAuthFilter.Builder<User>()
.setAuthenticator(new JWTDefaultAuthenticator())
.setAuthorizer(new UserAuthorizer()).setRealm("SUPER SECRET STUFF")
.buildAuthFilter()))));
Please note that the configuration class must contain a configuration setting:
String jwsSecretKey;
Here, the TokenResource is the token supplying resource, and the HelloResource is our test resource. User is the principal, like this:
public class User implements Principal {
private String name;
private String password;
...
}
And there is one class for communicating the JWT token:
public class JWTCredentials {
private String jwtToken;
...
}
TokenResource provides tokens for a user "test" with password "test":
#POST
#Path("{user}")
#PermitAll
public String createToken(#PathParam("user") String user, String password) {
if ("test".equals(user) && "test".equals(password)) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(this.secretKey);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
JwtBuilder builder = Jwts.builder().setIssuedAt(now).setSubject("test")
.signWith(signatureAlgorithm, signingKey);
return builder.compact();
}
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
And the HelloResource just echoes back the user:
#GET
#RolesAllowed({"ANY"})
public String hello(#Auth User user) {
return "hello user \"" + user.getName() + "\"";
}
JWTCredentialAuthFilter provides credentials for both authentication schemes:
#Priority(Priorities.AUTHENTICATION)
public class JWTCredentialAuthFilter<P extends Principal> extends AuthFilter<JWTCredentials, P> {
public static class Builder<P extends Principal>
extends AuthFilterBuilder<JWTCredentials, P, JWTCredentialAuthFilter<P>> {
#Override
protected JWTCredentialAuthFilter<P> newInstance() {
return new JWTCredentialAuthFilter<>();
}
}
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
final JWTCredentials credentials =
getCredentials(requestContext.getHeaders().getFirst(HttpHeaders.AUTHORIZATION));
if (!authenticate(requestContext, credentials, "JWT")) {
throw new WebApplicationException(
this.unauthorizedHandler.buildResponse(this.prefix, this.realm));
}
}
private static JWTCredentials getCredentials(String authLine) {
if (authLine != null && authLine.startsWith("Bearer ")) {
JWTCredentials result = new JWTCredentials();
result.setJwtToken(authLine.substring(7));
return result;
}
return null;
}
}
JWTAuthenticator is when JWT credentials are provided:
public class JWTAuthenticator implements Authenticator<JWTCredentials, User> {
private String secret;
public JWTAuthenticator(String jwtsecret) {
this.secret = jwtsecret;
}
#Override
public Optional<User> authenticate(JWTCredentials credentials) throws AuthenticationException {
try {
Claims claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(this.secret))
.parseClaimsJws(credentials.getJwtToken()).getBody();
User user = new User();
user.setName(claims.getSubject());
return Optional.ofNullable(user);
} catch (#SuppressWarnings("unused") ExpiredJwtException | UnsupportedJwtException
| MalformedJwtException | SignatureException | IllegalArgumentException e) {
return Optional.empty();
}
}
}
JWTDefaultAuthenticator is when no credentials are present, giving the code an empty user:
public class JWTDefaultAuthenticator implements Authenticator<JWTCredentials, User> {
#Override
public Optional<User> authenticate(JWTCredentials credentials) throws AuthenticationException {
return Optional.of(new User());
}
}
UserAuthorizer permits the "ANY" role, as long as the user is not null:
public class UserAuthorizer implements Authorizer<User> {
#Override
public boolean authorize(User user, String role) {
return user != null && "ANY".equals(role)
}
}
If all goes well,
curl -s -X POST -d 'test' http://localhost:8080/token/test
will give you something like:
eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MDk3MDYwMjYsInN1YiI6InRlc3QifQ.ZrRmWTUDpaA6JlU4ysIcFllxtqvUS2OPbCMJgyou_tY
and this query
curl -s -X POST -d 'xtest' http://localhost:8080/token/test
will fail with
{"code":401,"message":"HTTP 401 Unauthorized"}
(BTW, "test" in the URL is the user name and "test" in the post data is the password. As easy as basic auth, and can be configured for CORS.)
and the request
curl -s -X GET -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MDk3MDYwMjYsInN1YiI6InRlc3QifQ.ZrRmWTUDpaA6JlU4ysIcFllxtqvUS2OPbCMJgyou_tY' http://localhost:8080/hello
will show
hello user "test"
while
curl -s -X GET -H 'Authorization: Bearer invalid' http://localhost:8080/hello
and
curl -s -X GET http://localhost:8080/hello
will result in
{"code":403,"message":"User not authorized."}
Your missing this line in your configuration.
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
Related
I installed Vault locally. I was able to start local dev server and write/read some secrets into Vault kv based on this official tutorial https://learn.hashicorp.com/vault/
Then I wanted to create some very basic Java/Spring Boot demo client that would connect to my local Vault dev server in order to write/read secrets. I read Baeldung tutorial for inspiration https://www.baeldung.com/spring-vault.
This is my vault-config.properties:
vault.uri=http://127.0.0.1:8200
vault.token=s.EXg6MQwUuB63Z7Xra4zybOut (token generated after the latest start of server)
Then service class:
#Service
public class CredentialsService {
#Autowired
private VaultTemplate vaultTemplate;
public void secureCredentials(Credentials credentials) throws URISyntaxException {
vaultTemplate.write("credentials/myapp", credentials);
}
public Credentials accessCredentials() throws URISyntaxException {
VaultResponseSupport<Credentials> response = vaultTemplate.read("credentials/myapp", Credentials.class);
return response.getData();
}
}
Configuration class:
#Configuration
public class VaultConfig extends AbstractVaultConfiguration {
#Override
public ClientAuthentication clientAuthentication() {
return new TokenAuthentication("s.EXg6MQwUuB63Z7Xra4zybOut");
}
#Override
public VaultEndpoint vaultEndpoint() {
return VaultEndpoint.create("host", 8200);
}
}
and this:
#Configuration
#PropertySource(value = { "vault-config.properties" })
#Import(value = EnvironmentVaultConfiguration.class)
public class VaultEnvironmentConfig {
}
One domain object:
public class Credentials {
private String username;
private String password;
public Credentials() {
}
public Credentials(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
#Override
public String toString() {
return "Credential [username=" + username + ", password=" + password + "]";
}
}
And finally my main Spring Boot class:
#RestController
#ComponentScan
#SpringBootApplication
public class SpringVaultTutorial {
#Autowired
CredentialsService credentialsService;
#RequestMapping("/")
String home() throws URISyntaxException {
Credentials credentials = new Credentials("oliver","exxeta123");
credentialsService.secureCredentials(credentials);
return credentialsService.accessCredentials().getUsername().toString();
}
public static void main(String[] args) {
SpringApplication.run(SpringVaultTutorial.class, args);
}
}
Main class should write secret and then immediately read it and print username. But I am getting this error message:
There was an unexpected error (type=Internal Server Error, status=500).
I/O error on POST request for "https://host:8200/v1/credentials/myapp": host; nested exception is java.net.UnknownHostException: host
Does somebody have a clue what can be wrong?
EDIT:
Based on advice from Arun I followed this tutorial https://drissamri.be/blog/java/enable-https-in-spring-boot/
I have been trying both approaches.
1) Modify application.properties:
server.port: 8443
server.ssl.key-store: keystore.p12
server.ssl.key-store-password: oliver
server.ssl.keyStoreType: PKCS12
server.ssl.keyAlias: tomcat
security.require-ssl=true
After modification, when I call https://localhost:8443, I am getting Exception:
javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?
at sun.security.ssl.InputRecord.handleUnknownRecord(InputRecord.java:710) ~[na:1.8.0_121]
at sun.security.ssl.InputRecord.read(InputRecord.java:527) ~[na:1.8.0_121]
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:973) ~[na:1.8.0_121]
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375) ~[na:1.8.0_121]
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403) ~[na:1.8.0_121]
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387) ~[na:1.8.0_121]
2) Second approach based on tutorial is about adding ConnectorConfig class:
#Configuration
public class ConnectorConfig {
#Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat =
new TomcatServletWebServerFactory() {
#Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(redirectConnector());
return tomcat;
}
private Connector redirectConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8090);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}
}
But after calling localhost:8090 that redirects me to https://localhost:8443, I am getting the same error: javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?
at sun.security.ssl.InputRecord.handleUnknownRecord(InputRecord.java:710) ~
Now the question is: Do I have to configure something also on the Vault side regarding certificate? Or do you think there could be some certificate problem on Java client side? But I thing if there was Java certificate problem, exception would be thrown already during startup.
Problem solved. Now I am able to connect to local Vault from Java client. I paste code here if someone in the future wants to run simple Java Client - Vault demo.
Controller:
#RestController
#RequestMapping(Paths.ROOT)
#Api(value = Paths.ROOT, description = "Endpoint for core testing")
public class Controller {
#Autowired
CredentialsService credentialsService;
#GetMapping("/")
String home() throws URISyntaxException {
Credentials credentials = new Credentials("oliver", "exxeta123");
credentialsService.secureCredentials(credentials);
return credentialsService.accessCredentials().toString();
}
#GetMapping("/test")
public String test() throws IOException {
// http://127.0.0.1:8200/v1/sys/internal/ui/mounts/secret/mysecrets
VaultConfig vc = new VaultConfig();
String bearerToken = vc.clientAuthentication().login().getToken();
System.out.println(bearerToken);
// credentialsService.accessCredentials()
// Sending get request
//URL url = new URL("http://127.0.0.1:8200/v1/sys/internal/ui/mounts/secret/mysecrets");
// URL updated to match readme.adoc
URL url = new URL("http://127.0.0.1:8200/v1/kv/my-secret");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Authorization", "Bearer " + bearerToken);
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestMethod("GET");
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String output;
StringBuffer response = new StringBuffer();
while ((output = in.readLine()) != null) {
response.append(output);
}
in.close();
// printing result from response
return "Response: - " + response.toString();
}
#GetMapping(value = { "/add/{name}/{username}/{password}" })
public ResponseEntity<String> addKey(#PathVariable(value = "name", required = false, name = "name") String name,
#PathVariable(value = "username", required = false, name = "username") String username,
#PathVariable(value = "password", required = false, name = "password") String password) throws URISyntaxException {
Credentials credentials = new Credentials(username, password);
credentialsService.secureCredentials(name, credentials);
return new ResponseEntity<>("Add success: " + credentialsService.accessCredentials(name).getUsername(), HttpStatus.OK);
}
#GetMapping(value = {"/get", "/get/{name}"})
public ResponseEntity<Credentials> getKey(#PathVariable(value = "name", required = false, name = "name") String name) {
return new ResponseEntity<>(credentialsService.accessCredentials(name), HttpStatus.OK);
}
#GetMapping(value= {"/delete", "/delete/{name}"})
public String removeKey(#PathVariable(value = "name", required = false, name = "name") String name) {
return "Delete success: " + credentialsService.deleteCredentials(name);
}
}
Service:
#Service
public class CredentialsService {
private VaultTemplate vaultTemplate;
/**
* To Secure Credentials
*
* #param credentials
* #return VaultResponse
* #throws URISyntaxException
*/
public void secureCredentials(Credentials credentials) throws URISyntaxException {
//vaultTemplate.write("credentials/myapp", credentials);
initVaultTemplate();
vaultTemplate.write("kv/myapp", credentials);
}
public void secureCredentials(String storagePlace, Credentials credentials) {
initVaultTemplate();
vaultTemplate.write("kv/" + storagePlace, credentials);
}
/**
* To Retrieve Credentials
*
* #return Credentials
* #throws URISyntaxException
*/
public Credentials accessCredentials() throws URISyntaxException {
//VaultResponseSupport<Credentials> response = vaultTemplate.read("credentials/myapp", Credentials.class);
initVaultTemplate();
VaultResponseSupport<Credentials> response = vaultTemplate.read("kv/myapp", Credentials.class);
return response.getData();
// TODO special case when there are no values
}
/**
* #param nameOfsecrets key name
* #return if is presented or empty object
*/
public Credentials accessCredentials(String nameOfsecrets) {
initVaultTemplate();
VaultResponseSupport<Credentials> response = vaultTemplate.read("kv/" + nameOfsecrets, Credentials.class);
if (response != null) {
return response.getData();
} else {
return new Credentials();
}
}
public boolean deleteCredentials(String name) {
initVaultTemplate();
vaultTemplate.delete("kv/" + name);
return true;
}
}
private void initVaultTemplate() {
VaultEndpoint endpoint = new VaultEndpoint();
endpoint.setHost("localhost");
endpoint.setPort(8200);
endpoint.setScheme("http");
vaultTemplate = new VaultTemplate(endpoint, new VaultConfig().clientAuthentication());
}
VaultConfig:
#Configuration
public class VaultConfig extends AbstractVaultConfiguration {
#Override
public ClientAuthentication clientAuthentication() {
return new TokenAuthentication("00000000-0000-0000-0000-000000000000");
}
#Override
public VaultEndpoint vaultEndpoint() {
return VaultEndpoint.create("localhost", 8200);
}
}
VaultEnvironmentConfig:
#Configuration
#PropertySource(value = { "vault-config.properties" })
#Import(value = EnvironmentVaultConfiguration.class)
public class VaultEnvironmentConfig {
}
Main class:
#SpringBootApplication
#EnableSwagger2
public class SpringVaultTutorial {
public static void main(String[] args) {
SpringApplication.run(SpringVaultTutorial.class, args);
}
//SWAGGER DOCUMENTATION BEANS
// default group contains all endpoints
#Bean
public Docket defaultApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())//all
.build().apiInfo(apiInfo());
}
// Management group contains Spring Actuator endpoints
#Bean
public Docket swaggerAdminEndpoints() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName(Paths.ROOT)
.apiInfo(apiInfo())
.select()
.paths(PathSelectors.regex("/v1/.*"))
.build()
.forCodeGeneration(true);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Vault Demo Application")
.description("Demo Application using vault")
.version("1.0")
.build();
}
}
vault-config.properties:
vault.uri=http://127.0.0.1:8200
vault.token=00000000-0000-0000-0000-000000000000
application.properties:
server.port=8443
server.ssl.key-alias=selfsigned_localhost_sslserver
server.ssl.key-password=changeit
server.ssl.key-store=classpath:ssl-server.jks
server.ssl.key-store-provider=SUN
server.ssl.key-store-type=JKS
Paths:
public class Paths {
public static final String ROOT = "/v1";
}
Credentials:
public class Credentials {
private String username;
private String password;
public Credentials() {
}
public Credentials(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
#Override
public String toString() {
return "Credential [username=" + username + ", password=" + password + "]";
}
}
UnknownHostException is due to no server is available with the name 'host'. you can either add an entry in hosts file map to localhost. or try changing the host name while creating vault as
#Override
public VaultEndpoint vaultEndpoint() {
return VaultEndpoint.create("localhost", 8200);
}
I already search all around the places on internet how to add basic auth using retrofit2 but still no luck. I already implemented a simple login mechanism but basic auth must be use in order for successful login.
My model
public class ResObj {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
ApiUtils.java
public class ApiUtils {
public static final String BASE_URL = "xxxxx";
public static UserService getUserService(){
return RetrofitClient.getClient(BASE_URL).create(UserService.class);
}
}
RetrofitClient.java
public class RetrofitClient {
private static Retrofit retrofit = null;
public static Retrofit getClient(String url){
if(retrofit == null){
retrofit = new Retrofit.Builder().baseUrl(url).addConverterFactory(GsonConverterFactory.create()).build();
}
return retrofit;
}
}
UserService.java
public interface UserService {
#GET("login?username={username}&password={password}")
Call<ResObj> login(#Path("username") String username, #Path("password") String password);
}
LoginActivity.java
private void doLogin(String username, String password){
Call<ResObj> call = userService.login(username, password);
call.enqueue(new Callback<ResObj>() {
#Override
public void onResponse(Call<ResObj> call, Response<ResObj> response) {
if(response.isSuccessful()){
ResObj resObj = response.body();
if(resObj.getMessage().equals("true")){
Intent intent = new Intent(TextLoginActivity.this, MainActivity.class);
startActivity(intent);
} else {
Toast.makeText(TextLoginActivity.this, "The username and password is incorrect", Toast.LENGTH_SHORT).show();
}
} else{
Toast.makeText(TextLoginActivity.this, "Error! Please try again!", Toast.LENGTH_SHORT).show();
}
}
#Override
public void onFailure(Call<ResObj> call, Throwable t) {
Toast.makeText(TextLoginActivity.this, t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
As I see your login API request, it exposes the user's password and the username in the query. The least you can do is maybe encrypt it. But a much better solution would be to have a POST request instead of a GET request with the username and password in the body. You add the Authorization header to the API request as follows:
public interface UserService {
String authorization = "Authorization: Basic XXXXXX";
String contentType = "Content-Type: application/json";
// Static Header
#POST("login")
#Headers({
contentType,
authorization
})
Call<ResObj> login(#Body UserCredential userCred);
// Dynamic Header
#POST("login")
Call<ResObj> login(#Header("Authorization") String basicAuth, #Body UserCredential userCred);
}
The Body model can be:
public class UserCredential {
private String username, password;
public String getPassword() {
...encrypt your password here...
return encrypted_password;
}
}
Also, it is safe to put a null check before you do this: response.body(); in the API response.
I have a server that is just an API endpoint, no client front-end, no jsp, no html. It uses Spring Boot and I'm trying to secure it with Shiro. The relevent parts of my SpringBootServletInitializer look like this. I'm trying to get Shiro to return a 403 response if it fails the roles lookup as defined in BasicRealm. Yet it seems to default to redirecting to a non-existent login.jsp and no matter what solution I seem to use. I can't override that. Any help would be greatly appreciated.
#SpringBootApplication
public class RestApplication extends SpringBootServletInitializer {
...
#Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
Map<String, String> filterChain = new HashMap<>();
filterChain.put("/admin/**", "roles[admin]");
shiroFilter.setFilterChainDefinitionMap(filterChain);
shiroFilter.setSecurityManager(securityManager());
return shiroFilter;
}
#Bean
public org.apache.shiro.mgt.SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
CookieRememberMeManager rmm = new CookieRememberMeManager();
rmm.setCipherKey(Base64.decode("XXXXXXXXXXXXXXXXXXXXXX"));
securityManager.setRememberMeManager(rmm);
return securityManager;
}
#Bean(name = "userRealm")
#DependsOn("lifecycleBeanPostProcessor")
public BasicRealm userRealm() {
return new BasicRealm();
}
#Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
public class BasicRealm extends AuthorizingRealm {
private static Logger logger = UserService.logger;
private static final String REALM_NAME = "BASIC";
public BasicRealm() {
super();
}
#Override
protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String userid = upToken.getUsername();
User user = Global.INST.getUserService().getUserById(userid);
if (user == null) {
throw new UnknownAccountException("No account found for user [" + userid + "]");
}
return new SimpleAuthenticationInfo(userid, user.getHashedPass().toCharArray(), REALM_NAME);
}
#Override
protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
String userid = (String) principals.getPrimaryPrincipal();
if (userid == null) {
return new SimpleAuthorizationInfo();
}
return new SimpleAuthorizationInfo(Global.INST.getUserService().getRoles(userid));
}
}
OK, here is how I solved it. I created a class ...
public class AuthFilter extends RolesAuthorizationFilter {
private static final String MESSAGE = "Access denied.";
#Override
protected boolean onAccessDenied(final ServletRequest request, final ServletResponse response) throws IOException {
HttpServletResponse httpResponse ;
try {
httpResponse = WebUtils.toHttp(response);
}
catch (ClassCastException ex) {
// Not a HTTP Servlet operation
return super.onAccessDenied(request, response) ;
}
if (MESSAGE == null) {
httpResponse.sendError(403);
} else {
httpResponse.sendError(403, MESSAGE);
}
return false; // No further processing.
}
}
... and then in my shiroFilter() method above I added this code ...
Map<String, Filter> filters = new HashMap<>();
filters.put("roles", new AuthFilter());
shiroFilter.setFilters(filters);
... hope this helps someone else.
In Shiro 1.4+ you can set the login url in your application.properties:
https://github.com/apache/shiro/blob/master/samples/spring-boot-web/src/main/resources/application.properties#L20
Earlier versions you should be able to set ShiroFilterFactoryBean.setLoginUrl("/login")
https://shiro.apache.org/static/current/apidocs/org/apache/shiro/spring/web/ShiroFilterFactoryBean.html
I have an spring mvc web application in which users login to session "session.setAttribute" classically. Whenever I need loggedin user data I use this data.
Now I want to add android app and what I want to learn do I have to add additional methods for each android request and send user data within it?
Or Is there away to make a request to same methods.
What is the consept for this kind of cloud apps? Do I have to write different methods for android requests? Because it is not possible session.getAttribute when wemake an android request, it returns null.
User user = userService.getByUserNameAndPassword(userName, password);
if (user != null) {
if (user.isActive()) {
Account account = new Account(user, request.getRemoteAddr());
HttpSession httpSession = request.getSession(true);
AccountRegistry.add(httpSession);
httpSession.setAttribute(Constant.ACCOUNT, account);
result.put(Constant.REF, Constant.SUCCESS);
}
public class Account {
private UserRightsHandler userRightsService = null;
private User user;
private String ipAddress;
private boolean admin;
public Account(User user, String ipAddress) {
this.user = user;
this.ipAddress = ipAddress;
userRightsService = new UserRightsHandler(user);
setAdmin(userRightsService.isAdmin());
}
public UserRightsHandler getUserRightsService() {
return userRightsService;
}
public User getUser() {
return this.user;
}
public String getIpAddress() {
return ipAddress;
}
public boolean isAdmin() {
return admin;
}
private void setAdmin(boolean admin) {
this.admin = admin;
}
}
public class AccountRegistry {
private static final Map<String, HttpSession> sessions = new HashMap<String, HttpSession>();
public static void add(HttpSession session) {
sessions.put(session.getId(), session);
}
public static void remove(HttpSession session) {
if (session != null) {
sessions.remove(session.getId());
session.setAttribute(Constant.ACCOUNT, null);
session.invalidate();
}
}
public static HttpSession getByHttpSessionID(String httpSessionID) {
Set<String> keys = sessions.keySet();
Iterator it = keys.iterator();
while (it.hasNext()) {
String sID = (String) it.next();
HttpSession session = sessions.get(sID);
if (sID.equals(httpSessionID)) {
return session;
}
}
return null;
}
public static void removeByHttpSessionID(String httpSessionID) {
HttpSession session = getByHttpSessionID(httpSessionID);
remove(session);
}
public static Account getCurrentAccount() {
HttpServletRequest request = ContextFilter.getCurrentInstance().getRequest();
HttpSession session = request.getSession();
return (Account) session.getAttribute(Constant.ACCOUNT);
}
}
#RequestMapping(value = "/changeStatus", method = RequestMethod.POST)
public #ResponseBody
String changeStatus(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
User editor = AccountRegistry.getCurrentAccount().getUser();
}
You can ask user send their user and password at the start of Android app via custom authenticate request like /appLogin then if it is correct creditentals you can return a key to user (to app) and store it to some variable during app run. Then when user want to do something send a request to server you can send it to a function with mapping like /appExampleService then you can check at that function this key and device valid depending on how you handle custom login process then this function call existing function that is used for web browsers that have mapping /exampleService. For example;
#JsonSerialize
#RequestMapping("/appExampleService")
public int someServiceForAppClient(
#RequestParam(value = "key", required = true) String apikey,
#RequestParam(value = "param", required = true) String someParam{
String name=userDAO.getUsernameFromApiKey(apikey);
return someService(someParam, name);
}
#JsonSerialize
#RequestMapping("/exampleService")
public int someServiceForWebClient(
#RequestParam(value = "param", required = true) String someParam) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String name = auth.getName();
return someService(someParam, name);
}
public int someService(String someParam,String name){
return doBusiness(someParam, name);
}
userDAO is just something I created for to get info of user with given key. And there is a service for App login as well which return that key to user when he started the app send his username and pass
I'm using spring boot and i need to implement spring security with 3 fields authentication process username, password and corporate identifier as a hidden input in a form.
I implemented a custom usernamepasswordauthenticationfilter but it not seems to be enough to setup the security config.
EDIT :
Users don't seem to be authenticated ! because a can access to authenticated request defined in web config
EDIT 2 :
in my custom filter when a enter a valid user it's do execute on succesfulAuthentication. What i'm missing please provide me any help :(
Here were i am
#Repository
public class AuthenticationUserDetailsService implements UserDetailsService {
private static final Logger LOGGER = Logger.getLogger(AuthenticationUserDetailsService.class);
#Autowired
private UserRepository users;
private org.springframework.security.core.userdetails.User userdetails;
#Override
public UserDetails loadUserByUsername(String input) throws UsernameNotFoundException {
// TODO Auto-generated method stub
System.out.println(input);
String[] split = input.split(":");
if (split.length < 2) {
LOGGER.debug("User did not enter both username and corporate domain.");
throw new UsernameNotFoundException("no corporate identifier is specified");
}
String username = split[0];
String corporateId = split[1];
System.out.println("Username = " + username);
System.out.println("Corporate identifier = " + corporateId);
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
com.ubleam.corporate.server.model.User user;
user = checkUserDetail(username, corporateId);
if (user == null)
throw new NotAuthorizedException("Your are not allowed to access to this resource");
LOGGER.info("User email : " + user.getEmail() + "#User corporate : " + user.getCorporateId());
userdetails = new User(user.getEmail(), user.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, getAuthorities("ROLE_USER"));
return userdetails;
}
/**
*
* #param roles
* roles granted for user
* #return List of granted authorities
*
*/
public List<GrantedAuthority> getAuthorities(String roles) {
List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
authList.add(new SimpleGrantedAuthority(roles));
return authList;
}
/**
* User authentication details from database
*
* #param username
* to use for authentication
* #param coporateId
* corporate identifier of user
* #return found user in database
*/
private com.ubleam.corporate.server.model.User checkUserDetail(String username, String corporateId) {
com.ubleam.corporate.server.model.User user = users.findByEmailAndCorporateId(username, corporateId);
return user;
}
My custom filter :
public class PlatformAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final Logger LOGGER = Logger.getLogger(PlatformAuthenticationFilter.class);
private static final String LOGIN_SUCCESS_URL = "{0}/bleamcards/{1}/home";
private static final String LOGIN_ERROR_URL = "{0}/bleamcards/{1}/login?error";
private String parameter = "corporateId";
private String delimiter = ":";
private String corporateId;
#Override
protected String obtainUsername(HttpServletRequest request) {
String username = request.getParameter(getUsernameParameter());
String extraInput = request.getParameter(getParameter());
String combinedUsername = username + getDelimiter() + extraInput;
setCorporateId(extraInput);
LOGGER.info("Combined username = " + combinedUsername);
return combinedUsername;
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException, ServletException {
String contextPath = request.getContextPath();
String url = MessageFormat.format(LOGIN_SUCCESS_URL, contextPath, corporateId);
response.sendRedirect(url);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
String contextPath = request.getContextPath();
String url = MessageFormat.format(LOGIN_ERROR_URL, contextPath, corporateId);
response.sendRedirect(url);
}
public String getParameter() {
return parameter;
}
public void setParameter(String corporateId) {
this.parameter = corporateId;
}
public String getDelimiter() {
return delimiter;
}
public void setDelimiter(String delimiter) {
this.delimiter = delimiter;
}
public String getCorporateId() {
return corporateId;
}
public void setCorporateId(String corporateId) {
this.corporateId = corporateId;
}
}
And finally the web security config :
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Inject
private AuthenticationManagerBuilder auth;
#Inject
private UserDetailsService userDS;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/bleamcards/**/login", "/bleamcards/**/forgetpassword", "/bleamcards/**/register", "/css/**", "/js/**", "/images/**", "/webjars/**")
.permitAll().anyRequest().authenticated().and().addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class).formLogin().loginPage("/login")
.defaultSuccessUrl("/").permitAll().and().logout().permitAll();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.eraseCredentials(false);
auth.userDetailsService(userDS).passwordEncoder(new BCryptPasswordEncoder());
}
#Bean
#Override
public AuthenticationManager authenticationManager() throws Exception {
return auth.build();
}
#Bean
public PlatformAuthenticationFilter authenticationFilter() throws Exception {
PlatformAuthenticationFilter authFilter = new PlatformAuthenticationFilter();
authFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
authFilter.setAuthenticationManager(authenticationManager());
authFilter.setUsernameParameter("username");
authFilter.setPasswordParameter("password");
authFilter.setParameter("corporateId");
return authFilter;
}
#Override
protected UserDetailsService userDetailsService() {
return userDS;
}
I want users to be able to connect only to /login /register /forgetpasswod urls for their respective corporate platforms
Actually i manage to find a solution to my issue.
I added successHandler on successfulAuthentication was missing ! And a failureHandler too on unsuccessfulAuthentication methods.
Here is my new Authentication filter :
public class TwoFactorAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final String LOGIN_SUCCESS_URL = "{0}/bleamcards/{1}/home";
private static final String LOGIN_ERROR_URL = "{0}/bleamcards/{1}/login?error";
private String parameter = "corporateId";
private String delimiter = ":";
private String corporateId;
#Override
protected String obtainUsername(HttpServletRequest request) {
String username = request.getParameter(getUsernameParameter());
String extraInput = request.getParameter(getParameter());
String combinedUsername = username + getDelimiter() + extraInput;
setCorporateId(extraInput);
System.out.println("Combined username = " + combinedUsername);
return combinedUsername;
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain , Authentication authResult) throws IOException, ServletException {
String contextPath = request.getContextPath();
String url = MessageFormat.format(LOGIN_SUCCESS_URL, contextPath, corporateId);
setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler(url));
super.successfulAuthentication(request, response, chain, authResult);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
String contextPath = request.getContextPath();
String url = MessageFormat.format(LOGIN_ERROR_URL, contextPath, corporateId);
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(url));
super.unsuccessfulAuthentication(request, response, failed);
}
public String getParameter() {
return parameter;
}
public void setParameter(String corporateId) {
this.parameter = corporateId;
}
public String getDelimiter() {
return delimiter;
}
public void setDelimiter(String delimiter) {
this.delimiter = delimiter;
}
public String getCorporateId() {
return corporateId;
}
public void setCorporateId(String corporateId) {
this.corporateId = corporateId;
}
}
Did you check that your AuthenticationUserDetailsService code is actually been executed? If the framework is not invoking it this means that your configuration is not properly hooking that UserDetailsService. In your WebSecurityConfig I think you need to have this:
#Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManagerBean(); //not return auth.build();
}
I suggest you to take a look at this branch from Stormpath. There, they are configuring Spring Boot to use a custom AuthenticationProvider (similar to an UserDetailsService). That module uses and depends on this other Spring Security module.
Then, this sample Spring Security Example (note that it is not Spring Boot, but just Spring) will give you a complete example of the way the Spring Security Java Config is done. Please note that this Java Config extends this one which actually hides much of the actual internal configuration.
Disclaimer, I am an active Stormpath contributor.