it would be best if there is a document or example that is available to the public to show how to integrate Angular http(with credentials) with Spring Security.
I have a way to login which I will show the code below, but I think there must be a better way. Maybe with the option in the Http header withCredentials, but where you provide your credentials?
It is keeping idToken from external auth. service (Google+) and type ( determine type of the auth. service) in headers, so you dont need pass them as a request parameter or as a path variable.
Then in the backend (Spring Java), there is a spring AOP that save the user to the SecurityContext after verification.
Angular Http Call
import { Http, Headers, RequestOptions } from '#angular/http';
...
constructor(private http: Http...){...}
...
search(){
let options ;
if (this.loginService.user) {
let headers = new Headers({ 'idToken': this.loginService.user.idToken,'type':this.loginService.user.type});
options = new RequestOptions({ headers: headers });
}
return this.http
.get("searchurl",options)
...
GooglePlusAuthService
import java.util.Collections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
#Service
public class GooglePlusAuthService implements AuthenticationService{
private Logger logger = LoggerFactory.getLogger(GooglePlusAuthService.class);
private static String clientId;
#Value("${client_id.google}")
public void setClientId(String clientId){
GooglePlusAuthService.clientId=clientId;
}
#Override
public void login(String token) {
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory())//.setIssuer(clientId)
.setAudience(Collections.singletonList(clientId))
.build();
GoogleIdToken idToken;
try {
idToken = verifier.verify(token);
if (idToken != null) {
Payload payload = idToken.getPayload();
User user = new User();
user.setId(Authenticator.AUTH_TYPE_GOOGLE+"_"+payload.getSubject());
user.setUsername((String) payload.get("name"));
user.setToken(token);
AuthenticationUtils.setUser(user);
} else {
logger.info("Failed to login with Google plus. Invalid ID token.");
}
} catch (Exception e) {
e.printStackTrace();
logger.error("Failed to login with Google plus." + e.getMessage());
}
}
}
AuthenticationUtils
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
public class AuthenticationUtils {
public static void setUser(User user){
Authentication authentication = new UsernamePasswordAuthenticationToken(user, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
public static User getUser(){
if(SecurityContextHolder.getContext().getAuthentication()!=null)
return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return null;
}
}
This code has a bug which I am also trying to find out.
Why AuthenticationUtils.getUser() is giving me the last signed in user when I didn't provide any credential info. I just opened the url with a private browser and it gets me the last signed in user in the backend.
Related
I am trying to implement mfa authentication in my app using totp. Bellow is the library i use. All is going well for registering the user, i receive the qr code, scan it and get every 30 secs the code in google authenticator. When i am trying to login to verify the code, the code verification doesnt work (in auth service, method Verify). I've spent several hours but cant figure it out, tried different users, logs but without success.
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp</artifactId>
<version>1.7.1</version>
</dependency>
this is my code
AuthContoller.java
import com.example.jsonfaker.model.dto.LoginRequest;
import com.example.jsonfaker.model.dto.SignupRequest;
import com.example.jsonfaker.model.dto.VerifyRequest;
import com.example.jsonfaker.service.Exporter;
import com.example.jsonfaker.service.UserAuthService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
#RestController
#RequestMapping("/auth")
#CrossOrigin
public class AuthController {
private final Exporter exporter;
private final UserAuthService userAuthService;
public AuthController(Exporter exporter, UserAuthService userAuthService) {
this.exporter = exporter;
this.userAuthService = userAuthService;
}
#PostMapping("/login")
public ResponseEntity<String> authenticateUser(#Valid #RequestBody LoginRequest loginRequest) {
String response = userAuthService.login(loginRequest);
return ResponseEntity
.ok()
.body(response);
}
#PostMapping("/register2FA")
public ResponseEntity<byte[]> registerUser2FA(#Valid #RequestBody SignupRequest signupRequest) throws Exception {
userAuthService.register2FA(signupRequest);
byte[] qrCodeBytes = userAuthService.mfaAccountSetup(signupRequest.getUsername());
return ResponseEntity
.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=\""+exporter.exportFileNameQR() + ".png\"")
.body(qrCodeBytes);
}
#PostMapping("/register")
public ResponseEntity<?> registerUser(#Valid #RequestBody SignupRequest signupRequest) throws Exception {
userAuthService.simpleRegister(signupRequest);
return ResponseEntity.ok(HttpStatus.CREATED);
}
#PostMapping("/verify")
public ResponseEntity<String> authenticateUser2FA(#Valid #RequestBody VerifyRequest verifyRequest) throws Exception {
String response = userAuthService.verify(verifyRequest.getUsername(), verifyRequest.getCode());
return ResponseEntity
.ok()
.body(response);
}
}
this is my token manager
import dev.samstevens.totp.code.*;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.QrGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import dev.samstevens.totp.time.SystemTimeProvider;
import dev.samstevens.totp.time.TimeProvider;
import dev.samstevens.totp.util.Utils;
import org.springframework.stereotype.Service;
#Service("mfaTokenManager")
public class DefaultMFATokenManager implements MFATokenManager {
private final SecretGenerator secretGenerator;
private final QrGenerator qrGenerator;
private final CodeVerifier codeVerifier;
public DefaultMFATokenManager(SecretGenerator secretGenerator, QrGenerator qrGenerator, CodeVerifier codeVerifier) {
this.secretGenerator = secretGenerator;
this.qrGenerator = qrGenerator;
this.codeVerifier = codeVerifier;
}
#Override
public String generateSecretKey() {
return secretGenerator.generate();
}
#Override
public String getQRCode(String secret) throws QrGenerationException {
QrData data = new QrData.Builder().label("MFA")
.secret(secret)
.issuer("Daniel token")
.algorithm(HashingAlgorithm.SHA1)
.digits(6)
.period(30)
.build();
return Utils.getDataUriForImage(
qrGenerator.generate(data),
qrGenerator.getImageMimeType()
);
}
#Override
public boolean verifyTotp(String code, String secret) {
TimeProvider timeProvider = new SystemTimeProvider();
CodeGenerator codeGenerator = new DefaultCodeGenerator();
CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
System.out.println(timeProvider.getTime());
System.out.println(codeGenerator);
return verifier.isValidCode(secret, code);
}
}
this is my auth service
import com.example.jsonfaker.model.Roles;
import com.example.jsonfaker.model.SystemUser;
import com.example.jsonfaker.model.dto.LoginRequest;
import com.example.jsonfaker.model.dto.SignupRequest;
import com.example.jsonfaker.model.dto.TokenResponse;
import com.example.jsonfaker.repository.RolesRepository;
import com.example.jsonfaker.repository.SystemUserRepository;
import com.example.jsonfaker.security.AuthoritiesConstants;
import com.example.jsonfaker.security.jwt.JwtUtils;
import com.example.jsonfaker.twoFA.MFATokenManager;
import com.example.jsonfaker.twoFA.MfaTokenData;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.stream.Collectors;
import static java.util.Objects.nonNull;
#Service
public class UserAuthService {
private final SystemUserRepository systemUserRepository;
private final RolesRepository rolesRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final MFATokenManager mfaTokenManager;
private final AuthenticationManager authenticationManager;
private final LoginUserService loginUserService;
private final JwtUtils jwtUtils;
public UserAuthService(SystemUserRepository systemUserRepository, RolesRepository rolesRepository, BCryptPasswordEncoder bCryptPasswordEncoder, MFATokenManager mfaTokenManager, AuthenticationManager authenticationManager, LoginUserService loginUserService, JwtUtils jwtUtils) {
this.systemUserRepository = systemUserRepository;
this.rolesRepository = rolesRepository;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.mfaTokenManager = mfaTokenManager;
this.authenticationManager = authenticationManager;
this.loginUserService = loginUserService;
this.jwtUtils = jwtUtils;
}
public void simpleRegister(SignupRequest signupRequest) throws Exception {
if(systemUserRepository.findByUsername(signupRequest.getUsername()).isPresent()){
throw new Exception("User with this username exists");
}
Roles simpleUserRole = new Roles();
simpleUserRole.setName(AuthoritiesConstants.USER);
SystemUser user = new SystemUser();
user.setPassword(bCryptPasswordEncoder.encode(signupRequest.getPassword()));
user.setUsername(signupRequest.getUsername());
user.setAuthorities(rolesRepository.findAllByName("ROLE_USER").stream().collect(Collectors.toSet()));
user.setSecret(mfaTokenManager.generateSecretKey());
systemUserRepository.save(user);
}
public void register2FA(SignupRequest signupRequest) throws Exception {
if(systemUserRepository.findByUsername(signupRequest.getUsername()).isPresent()){
throw new Exception("User with this username exists");
}
Roles simpleUserRole = new Roles();
simpleUserRole.setName(AuthoritiesConstants.USER);
SystemUser user = new SystemUser();
user.setPassword(bCryptPasswordEncoder.encode(signupRequest.getPassword()));
user.setUsername(signupRequest.getUsername());
user.setAuthorities(rolesRepository.findAllByName("ROLE_USER").stream().collect(Collectors.toSet()));
user.setTwoFAisEnabled(Boolean.TRUE);
user.setSecret(mfaTokenManager.generateSecretKey());
systemUserRepository.save(user);
}
public byte[] mfaAccountSetup(String username) throws Exception {
SystemUser user = systemUserRepository.findByUsername(username).get();
if (!nonNull(user)){
throw new Exception("Unable to find user with this username");
}
if(!user.isTwoFAisEnabled()){
throw new Exception("2FA is not enabled for this account");
}
MfaTokenData token = new MfaTokenData(mfaTokenManager.getQRCode(user.getSecret()), user.getSecret());
System.out.println("Mfa code :" +token.getMfaCode());
String base64Image = token.getQrCode().split(",")[1];
byte[] imageBytes = javax.xml.bind.DatatypeConverter.parseBase64Binary(base64Image);
return imageBytes;
}
public String login(LoginRequest loginRequest){
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
if(systemUserRepository.findByUsername(loginRequest.getUsername()).get().isTwoFAisEnabled()){
return "verify code now";
}
SystemUser userDetails = (SystemUser) authentication.getPrincipal();
String jwt = jwtUtils.generateJwtToken(userDetails);
return new TokenResponse(jwt).toString();
}
public String verify(String username, String code) throws Exception {
SystemUser user = systemUserRepository.findByUsername(username).get();
if (!nonNull(user)){
throw new Exception("Unable to find user with this username");
}
if (!mfaTokenManager.verifyTotp(code, user.getSecret())){
return "unable to auth";
}
return "token here";
}
}
Finally found the problem, on my phone the time was delayed by 2 minutes, I've set it to be the same as on my computer and worked. The problem is that when validating the token the app uses an interval of 30 seconds for each token generation, and if the delay on the phone or other device is bigger than 30 sec in future or past the timestamp doesnt match the one used for verification.
Here is the documentation for the library i used, make sure to read it carefully before using it.
https://github.com/samdjstevens/java-totp
Here is the article which i followed in my project:
https://www.javadevjournal.com/spring-security/two-factor-authentication-with-spring-security/
Useful reading before starting a project using TOTP:
https://www.freecodecamp.org/news/how-time-based-one-time-passwords-work-and-why-you-should-use-them-in-your-app-fdd2b9ed43c3/
A youtube video about 2FA:
https://www.youtube.com/watch?v=ZXFYT-BG2So
I'm trying to work out a simple web app with angular as the front end and Spring Boot as the back end. My pre-flight request succeeds and I receive a valid token, however when trying to make a subsequent request to a route I get a 404. When I try to curl the url I get a 500 with the message "Missing or invalid authorization header" which seems to me to be saying that the route does indeed exist and is listening, but something else is wrong.
First of all the Typescript. Here is my login.component.ts
import { Component } from "#angular/core";
import { Observable } from "rxjs/index";
import { LoginService } from "../services/login.service";
#Component({
selector: "login",
templateUrl: "./login.component.html"
})
export class Login {
private model = {"username": "", "password": ""};
private currentUserName;
constructor (private loginService: LoginService) {
this.currentUserName = localStorage.getItem("currentUserName");
}
public onSubmit() {
console.log("submitting");
this.loginService.sendCredential(this.model).subscribe(
data => {
console.log(data);
localStorage.setItem("token", data);
console.log("Setting token");
this.loginService.sendToken(localStorage.getItem("token")).subscribe(
data => {
this.currentUserName = this.model.username;
localStorage.setItem("currentUserName", this.model.username);
this.model.username = "";
this.model.password = "";
},
error => console.log(error)
);
},
error => {
console.log("oh no");
console.log(error)
}
);
}
}
And then my LoginService
import { Injectable } from "#angular/core";
import { HttpClient } from '#angular/common/http';
import { HttpHeaders } from '#angular/common/http';
import { Observable } from "rxjs/index";
#Injectable()
export class LoginService {
token: string;
constructor (private http: HttpClient) {}
public sendCredential(model): Observable<String> {
const tokenUrlPreFlight = "http://localhost:8080/users/login/";
const httpOptions: {} = {
headers: new HttpHeaders({
'ContentType': 'application/json'
})
};
return this.http.post<String>(tokenUrlPreFlight, model, httpOptions);
}
public sendToken(token) {
const tokenUrlPostFlight = "http://localhost:8080/rest/users/";
console.log("Bearer " + token);
const httpOptions: {} = {
headers: new HttpHeaders({
'Authorization': 'Bearer ' + token
})
};
return this.http.get(tokenUrlPostFlight, httpOptions);
}
public logout(): void {
localStorage.setItem("token", "");
localStorage.setItem("currentUserName", "");
alert("You just logged out");
}
public checkLogin(): boolean {
if(localStorage.getItem("currentUserName") != null && localStorage.getItem("currentUserName") != "" && localStorage.getItem("token") != null && localStorage.getItem("token") != "") {
console.log(localStorage.getItem("currentUserName"));
console.log(localStorage.getItem("token"));
return true;
}
return false;
}
}
And now for the java. First here is my entry point:
import com.acb.app.configuration.JwtFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
#SpringBootApplication
public class App {
#Bean
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean<JwtFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new JwtFilter());
filterRegistrationBean.addUrlPatterns("/rest/*");
return filterRegistrationBean;
}
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
My UserController.java
import com.acb.app.model.User;
import io.jsonwebtoken.*;
import com.acb.maki.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.ServletException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
#RestController
#RequestMapping("/users")
public class UserController {
#Autowired
private UserService userService;
#GetMapping("")
public List<User> userIndex() {
return userService.getAllUsers();
}
#GetMapping("{username}")
public Optional<User> getUserByUsername(#RequestBody String username) {
return userService.findByUsername(username);
}
#PostMapping("login")
public String login(#RequestBody Map<String, String> json) throws ServletException {
if(json.get("username") == null || json.get("password") == null) {
throw new ServletException("Please fill in username and password");
}
final String username = json.get("username");
final String password = json.get("password");
Optional<User> optionalUser = userService.findByUsername(username);
if(!optionalUser.isPresent()) {
throw new ServletException("Username not found");
}
User user = optionalUser.get();
if(!password.equals(user.getPassword())) {
throw new ServletException("Invalid login. Please check username and password");
}
final String response = Jwts.builder().setSubject(username).claim("roles", "user").setIssuedAt(new Date()).signWith(SignatureAlgorithm.HS256, "secretKey").compact();
final String jsonResponse = String.format("\"%s\"", response);
System.out.println(jsonResponse);
return jsonResponse;
}
#PostMapping(value="/register")
public User register(#RequestBody User user) {
return userService.save(user);
}
}
And last but not least my JwtFilter.java
import io.jsonwebtoken.*;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final String authHeader = request.getHeader("Authorization");
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
filterChain.doFilter(servletRequest, servletResponse);
} else {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new ServletException("Missing or invalid authorization header");
}
final String token = authHeader.substring(7);
try {
final JwtParser jwtParser = Jwts.parser();
final Jws<Claims> claimsJws = jwtParser.setSigningKey("secretKey").parseClaimsJws(token);
final Claims claims = claimsJws.getBody();
request.setAttribute("claims", claims);
} catch (final SignatureException e) {
throw new ServletException("Invalid token.");
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
I am very aware of the bad practices here with comparing plain text passwords, etc. I'm just trying to get this to work for the time being. The token is correctly generated and returned. The token is also set correctly to local storage, but upon making the get request to the /rest/users route a 404 is returned. There are no errors on the java side.
So as I expected, I am indeed an idiot. As the user mentioned above my service maps to /users/ and what I needed is the protected version /rest/users. So next to my UserController I created UserResource.java which looks like so:
import com.acb.app.model.User;
import com.acb.app.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
#RestController
#RequestMapping("/rest")
public class UserResource {
#Autowired
private UserService userService;
#RequestMapping("/users")
public List<User> findAllUsers() {
return userService.getAllUsers();
}
}
And this makes use of the JwtFilter and allows me to protect my routes. Hopefully this will be helpful to someone else.
I have spring boot rest api (resources) which uses another spring boot authorisation server, I have added Swagger config to the resource application to get a nice and quick documentation/test platform for the rest API. my Swagger config looks like this:
#Configuration
#EnableSwagger2
public class SwaggerConfig {
#Autowired
private TypeResolver typeResolver;
#Value("${app.client.id}")
private String clientId;
#Value("${app.client.secret}")
private String clientSecret;
#Value("${info.build.name}")
private String infoBuildName;
public static final String securitySchemaOAuth2 = "oauth2";
public static final String authorizationScopeGlobal = "global";
public static final String authorizationScopeGlobalDesc = "accessEverything";
#Bean
public Docket api() {
List<ResponseMessage> list = new java.util.ArrayList<ResponseMessage>();
list.add(new ResponseMessageBuilder()
.code(500)
.message("500 message")
.responseModel(new ModelRef("JSONResult«string»"))
.build());
list.add(new ResponseMessageBuilder()
.code(401)
.message("Unauthorized")
.responseModel(new ModelRef("JSONResult«string»"))
.build());
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(securitySchema()))
.securityContexts(Collections.singletonList(securityContext()))
.pathMapping("/")
.directModelSubstitute(LocalDate.class,String.class)
.genericModelSubstitutes(ResponseEntity.class)
.alternateTypeRules(
newRule(typeResolver.resolve(DeferredResult.class,
typeResolver.resolve(ResponseEntity.class, WildcardType.class)),
typeResolver.resolve(WildcardType.class)))
.useDefaultResponseMessages(false)
.apiInfo(apiInfo())
.globalResponseMessage(RequestMethod.GET,list)
.globalResponseMessage(RequestMethod.POST,list);
}
private OAuth securitySchema() {
List<AuthorizationScope> authorizationScopeList = newArrayList();
authorizationScopeList.add(new AuthorizationScope("global", "access all"));
List<GrantType> grantTypes = newArrayList();
final TokenRequestEndpoint tokenRequestEndpoint = new TokenRequestEndpoint("http://server:port/oauth/token", clientId, clientSecret);
final TokenEndpoint tokenEndpoint = new TokenEndpoint("http://server:port/oauth/token", "access_token");
AuthorizationCodeGrant authorizationCodeGrant = new AuthorizationCodeGrant(tokenRequestEndpoint, tokenEndpoint);
grantTypes.add(authorizationCodeGrant);
OAuth oAuth = new OAuth("oauth", authorizationScopeList, grantTypes);
return oAuth;
}
private SecurityContext securityContext() {
return SecurityContext.builder().securityReferences(defaultAuth())
.forPaths(PathSelectors.ant("/api/**")).build();
}
private List<SecurityReference> defaultAuth() {
final AuthorizationScope authorizationScope =
new AuthorizationScope(authorizationScopeGlobal, authorizationScopeGlobalDesc);
final AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Collections
.singletonList(new SecurityReference(securitySchemaOAuth2, authorizationScopes));
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(“My rest API")
.description(" description here … ”)
.termsOfServiceUrl("https://www.example.com/")
.contact(new Contact(“XXXX XXXX”,
"http://www.example.com", “xxxx#example.com”))
.license("license here”)
.licenseUrl("https://www.example.com")
.version("1.0.0")
.build();
}
}
The way I get the access token from the Authorisation server is by using http POST to this link with basic authorisation in the header for clientid/clientpass:
http://server:port/oauth/token?grant_type=password&username=<username>&password=<password>
the response is something like:
{
"access_token": "e3b98877-f225-45e2-add4-3c53eeb6e7a8",
"token_type": "bearer",
"refresh_token": "58f34753-7695-4a71-c08a-d40241ec3dfb",
"expires_in": 4499,
"scope": "read trust write"
}
in Swagger UI I can see an Authorisation button, which opens a dialog to make the authorisation request, but it is not working and directing me to a link as following,
http://server:port/oauth/token?response_type=code&redirect_uri=http%3A%2F%2Fserver%3A8080%2Fwebjars%2Fspringfox-swagger-ui%2Fo2c.html&realm=undefined&client_id=undefined&scope=global%2CvendorExtensions&state=oauth
what I am missing here?
After 8 months, finally the password flow is supported in Swagger UI, here is the final code and settings which works for me:
1) Swagger Config:
package com.example.api;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMethod;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.GrantType;
import springfox.documentation.service.OAuth;
import springfox.documentation.service.ResourceOwnerPasswordCredentialsGrant;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.ApiKeyVehicle;
import springfox.documentation.swagger.web.SecurityConfiguration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.Collections;
import java.util.List;
import static com.google.common.collect.Lists.*;
#Configuration
#EnableSwagger2
public class SwaggerConfig {
#Value("${app.client.id}")
private String clientId;
#Value("${app.client.secret}")
private String clientSecret;
#Value("${info.build.name}")
private String infoBuildName;
#Value("${host.full.dns.auth.link}")
private String authLink;
#Bean
public Docket api() {
List<ResponseMessage> list = new java.util.ArrayList<>();
list.add(new ResponseMessageBuilder().code(500).message("500 message")
.responseModel(new ModelRef("Result")).build());
list.add(new ResponseMessageBuilder().code(401).message("Unauthorized")
.responseModel(new ModelRef("Result")).build());
list.add(new ResponseMessageBuilder().code(406).message("Not Acceptable")
.responseModel(new ModelRef("Result")).build());
return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any()).build().securitySchemes(Collections.singletonList(securitySchema()))
.securityContexts(Collections.singletonList(securityContext())).pathMapping("/")
.useDefaultResponseMessages(false).apiInfo(apiInfo()).globalResponseMessage(RequestMethod.GET, list)
.globalResponseMessage(RequestMethod.POST, list);
}
private OAuth securitySchema() {
List<AuthorizationScope> authorizationScopeList = newArrayList();
authorizationScopeList.add(new AuthorizationScope("read", "read all"));
authorizationScopeList.add(new AuthorizationScope("trust", "trust all"));
authorizationScopeList.add(new AuthorizationScope("write", "access all"));
List<GrantType> grantTypes = newArrayList();
GrantType creGrant = new ResourceOwnerPasswordCredentialsGrant(authLink+"/oauth/token");
grantTypes.add(creGrant);
return new OAuth("oauth2schema", authorizationScopeList, grantTypes);
}
private SecurityContext securityContext() {
return SecurityContext.builder().securityReferences(defaultAuth()).forPaths(PathSelectors.ant("/user/**"))
.build();
}
private List<SecurityReference> defaultAuth() {
final AuthorizationScope[] authorizationScopes = new AuthorizationScope[3];
authorizationScopes[0] = new AuthorizationScope("read", "read all");
authorizationScopes[1] = new AuthorizationScope("trust", "trust all");
authorizationScopes[2] = new AuthorizationScope("write", "write all");
return Collections.singletonList(new SecurityReference("oauth2schema", authorizationScopes));
}
#Bean
public SecurityConfiguration securityInfo() {
return new SecurityConfiguration(clientId, clientSecret, "", "", "", ApiKeyVehicle.HEADER, "", " ");
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder().title("My API title").description("")
.termsOfServiceUrl("https://www.example.com/api")
.contact(new Contact("Hasson", "http://www.example.com", "hasson#example.com"))
.license("Open Source").licenseUrl("https://www.example.com").version("1.0.0").build();
}
}
2) in POM use this Swagger UI version 2.7.0:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-bean-validators</artifactId>
<version>2.7.0</version>
</dependency>
3) in the application.properties add the following properties:
host.full.dns.auth.link=http://oauthserver.example.com:8081
app.client.id=test-client
app.client.secret=clientSecret
auth.server.schem=http
4) in the Authorisation server add a CORS filter:
package com.example.api.oauth2.oauth2server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Allows cross origin for testing swagger docs using swagger-ui from local file
* system
*/
#Component
public class CrossOriginFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(CrossOriginFilter.class);
#Override
public void init(FilterConfig filterConfig) throws ServletException {
// Called by the web container to indicate to a filter that it is being
// placed into service.
// We do not want to do anything here.
}
#Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
log.info("Applying CORS filter");
HttpServletResponse response = (HttpServletResponse) resp;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "0");
chain.doFilter(req, resp);
}
#Override
public void destroy() {
// Called by the web container to indicate to a filter that it is being
// taken out of service.
// We do not want to do anything here.
}
}
If you run with these settings you will get the authorize button in the link http://apiServer.example.com:8080/swagger-ui.html#/ (if you run on 8080) as follows:
Then when you click on the authorize button you will get the following dialogue, add the data for your username/password and the client id and the client secret, the type has to be request body, I am not sure why but this is what works with me, although I thought it should be basic auth as this is how the client secret is sent, anyway this is how Swagger-ui works with password flow and all your API endpoints are working again. Happy swaggering!!! :)
I am not sure as what was the issue for you but Authorize button is working for me for swagger version 2.7.0, though I have to get JWT token manually.
First I make a hit for auth token then I insert token like below,
Key here is that my tokens are JWT and I was not able to insert token value after Bearer ** and changing **api_key name to Authorization and that I achieved with below Java configuration ,
#Bean
public SecurityConfiguration securityInfo() {
return new SecurityConfiguration(null, null, null, null, "", ApiKeyVehicle.HEADER,"Authorization",": Bearer");
}
There seems a bug in swagger about scope separator which by default is : . In my config, I tried to modify it to : Bearerbut that is not happening so I have to enter that on UI .
The best way so far to work with oAuth2 Authorisation is by using Swagger Editor, I have installed Swagger Editor quickly in Docker (from here), then used the import parameter to download the API JSON descriptor (your API should include CORS filter), then I can get Swagger Documentation and an interface where I can add a token which I get using curl, postman, or Firefox rest client.
The link which I am using now looks like this
http://docker.example.com/#/?import=http://mywebserviceapi.example.com:8082/v2/api-docs&no-proxy
the interface in Swagger Editor to enter the token looks like this:
if there are better solutions, or workaround please post your answer here.
This is a bug on swagger-ui 2.6.1 which sends vendorExtensions scope everytime. And that causes the requests get out of scope which results in rejected requests. Since swagger can't get the access token it can't pass oauth2
Upgrading on maven should solve the problem. Minimum version should be 2.7.0
We were testing a REST webservice developed in jersey through postman rest client. It is a POST method and is annotated with #RolesAllowed. The full annotation the method is as follows:
#POST
#Path("/configuration")
#RolesAllowed("admin")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
When I requested this http://baseurl/configuration with the expected HTTP body content, I got 403 response(it is expected since it is allowed only for admin as it seems).
My doubt is how to access this service with the specified role via rest client.
So it seems like you set up the RolesAllowedDynamicFeature, but you have no authentication happening to set up the user and roles. What the RolesAllowedDynamicFeature does is lookup the SecurityContext, and calls the SecurityContext.isUserInRole(<"admin">) to see if the user in the SecurityContext has the role.
I imagine you don't know how the SecurityContext is set. There are a couple of ways. The first is through the servlet authentication mechanism. You can see more at Securing Web Applications from the Java EE tutorial.
Basically you need to set up a security realm or security domain on the server. Every server has it's own specific way of setting it up. You can see an example here or how it would be done with Tomcat.
Basically the realm/domain contains the users allowed to access the web app. Those users have associated roles. When the servlet container does the authentication, whether it be Basic authentication or Form authentication, it looks up the user from the credentials, and if the user is authenticated, the user and its roles are associated with the request. Jersey gathers this information and puts it into the SecurityContext for the request.
If this seems a bit complicated, an easier way to just forget the servlet container authentication and just create a Jersey filter, where you set the SecurityContext yourself. You can see an example here. You can use whatever authentication scheme you want. The important part is setting the SecurityContext with the user information, wherever you get it from, maybe a service that accesses a data store.
See Also:
securing rest services in Jersey
UPDATE
Here is a complete example of the second option using the filter. The test is run by Jersey Test Framework. You can run the test as is
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Priority;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Priorities;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.DatatypeConverter;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.internal.util.Base64;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.test.JerseyTest;
import static junit.framework.Assert.*;
import org.junit.Test;
public class BasicAuthenticationTest extends JerseyTest {
#Provider
#Priority(Priorities.AUTHENTICATION)
public static class BasicAuthFilter implements ContainerRequestFilter {
private static final Logger LOGGER = Logger.getLogger(BasicAuthFilter.class.getName());
#Inject
private UserStore userStore;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String authentication = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authentication == null) {
throw new AuthenticationException("Authentication credentials are required");
}
if (!authentication.startsWith("Basic ")) {
return;
}
authentication = authentication.substring("Basic ".length());
String[] values = new String(DatatypeConverter.parseBase64Binary(authentication),
Charset.forName("ASCII")).split(":");
if (values.length < 2) {
throw new WebApplicationException(400);
}
String username = values[0];
String password = values[1];
LOGGER.log(Level.INFO, "{0} - {1}", new Object[]{username, password});
User user = userStore.getUser(username);
if (user == null) {
throw new AuthenticationException("Authentication credentials are required");
}
if (!user.password.equals(password)) {
throw new AuthenticationException("Authentication credentials are required");
}
requestContext.setSecurityContext(new MySecurityContext(user));
}
}
static class MySecurityContext implements SecurityContext {
private final User user;
public MySecurityContext(User user) {
this.user = user;
}
#Override
public Principal getUserPrincipal() {
return new Principal() {
#Override
public String getName() {
return user.username;
}
};
}
#Override
public boolean isUserInRole(String role) {
return role.equals(user.role);
}
#Override
public boolean isSecure() { return true; }
#Override
public String getAuthenticationScheme() {
return "Basic";
}
}
static class AuthenticationException extends WebApplicationException {
public AuthenticationException(String message) {
super(Response
.status(Status.UNAUTHORIZED)
.header("WWW-Authenticate", "Basic realm=\"" + "Dummy Realm" + "\"")
.type("text/plain")
.entity(message)
.build());
}
}
class User {
public final String username;
public final String role;
public final String password;
public User(String username, String password, String role) {
this.username = username;
this.password = password;
this.role = role;
}
}
class UserStore {
public final Map<String, User> users = new ConcurrentHashMap<>();
public UserStore() {
users.put("peeskillet", new User("peeskillet", "secret", "USER"));
users.put("stackoverflow", new User("stackoverflow", "superSecret", "ADMIN"));
}
public User getUser(String username) {
return users.get(username);
}
}
private static final String USER_RESPONSE = "Secured User Stuff";
private static final String ADMIN_RESPONSE = "Secured Admin Stuff";
private static final String USER_ADMIN_STUFF = "Secured User Admin Stuff";
#Path("secured")
public static class SecuredResource {
#GET
#Path("userSecured")
#RolesAllowed("USER")
public String getUser() {
return USER_RESPONSE;
}
#GET
#Path("adminSecured")
#RolesAllowed("ADMIN")
public String getAdmin() {
return ADMIN_RESPONSE;
}
#GET
#Path("userAdminSecured")
#RolesAllowed({"USER", "ADMIN"})
public String getUserAdmin() {
return USER_ADMIN_STUFF;
}
}
#Override
public ResourceConfig configure() {
return new ResourceConfig(SecuredResource.class)
.register(BasicAuthFilter.class)
.register(RolesAllowedDynamicFeature.class)
.register(new AbstractBinder(){
#Override
protected void configure() {
bind(new UserStore()).to(UserStore.class);
}
});
}
static String getBasicAuthHeader(String username, String password) {
return "Basic " + Base64.encodeAsString(username + ":" + password);
}
#Test
public void should_return_403_with_unauthorized_user() {
Response response = target("secured/userSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("stackoverflow", "superSecret"))
.get();
assertEquals(403, response.getStatus());
}
#Test
public void should_return_200_response_with_authorized_user() {
Response response = target("secured/userSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("peeskillet", "secret"))
.get();
assertEquals(200, response.getStatus());
assertEquals(USER_RESPONSE, response.readEntity(String.class));
}
#Test
public void should_return_403_with_unauthorized_admin() {
Response response = target("secured/adminSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("peeskillet", "secret"))
.get();
assertEquals(403, response.getStatus());
}
#Test
public void should_return_200_response_with_authorized_admin() {
Response response = target("secured/adminSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("stackoverflow", "superSecret"))
.get();
assertEquals(200, response.getStatus());
assertEquals(ADMIN_RESPONSE, response.readEntity(String.class));
}
}
Here is the only dependency needed to run the test
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>${jersey2.version}</version>
<scope>test</scope>
</dependency>
I want to expose a resource using RESTlet with a fine-grained authentication. My ServerResource should be accessable via GET only for authenticated members (using BASIC Authentication). However, requests using POST should be available also for callers without any authentication.
In order to clearify:
http://path/myapp/user should allow anyone to register using POST, but only registered members should be able to GET a list of all users.
I'm unfortunately not much into RESTlet and I only find examples using coarser authentication for whole Restlets or Routers.
So how do I enable optional authentication for resources and check them on a per-method level?
Thanks in advance!
To do basic authentication in RESTlet 2.0 (I assume you're using 2.0 since you mention ServerResource), you need to use a ChallengeAuthenticator. If this is configured with optional = true then authentication will only be requested if you invoke ChallengeAuthenticator.challenge().
You can create your application with an authenticate() method, and call this whenever you need access to a resource to be secured:
Application:
package example;
import org.restlet.*;
import org.restlet.data.ChallengeScheme;
import org.restlet.routing.Router;
import org.restlet.security.*;
public class ExampleApp extends Application {
private ChallengeAuthenticator authenticatior;
private ChallengeAuthenticator createAuthenticator() {
Context context = getContext();
boolean optional = true;
ChallengeScheme challengeScheme = ChallengeScheme.HTTP_BASIC;
String realm = "Example site";
// MapVerifier isn't very secure; see docs for alternatives
MapVerifier verifier = new MapVerifier();
verifier.getLocalSecrets().put("user", "password".toCharArray());
ChallengeAuthenticator auth = new ChallengeAuthenticator(context, optional, challengeScheme, realm, verifier) {
#Override
protected boolean authenticate(Request request, Response response) {
if (request.getChallengeResponse() == null) {
return false;
} else {
return super.authenticate(request, response);
}
}
};
return auth;
}
#Override
public Restlet createInboundRoot() {
this.authenticatior = createAuthenticator();
Router router = new Router();
router.attach("/user", UserResource.class);
authenticatior.setNext(router);
return authenticatior;
}
public boolean authenticate(Request request, Response response) {
if (!request.getClientInfo().isAuthenticated()) {
authenticatior.challenge(response, false);
return false;
}
return true;
}
}
Resource:
package example;
import org.restlet.data.MediaType;
import org.restlet.representation.EmptyRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.resource.ServerResource;
public class UserResource extends ServerResource {
#Override
public Representation get() {
ExampleApp app = (ExampleApp) getApplication();
if (!app.authenticate(getRequest(), getResponse())) {
// Not authenticated
return new EmptyRepresentation();
}
// Generate list of users
// ...
}
#Override
public Representation post(Representation entity) {
// Handle post
// ...
}
}
I'm presently using Restlet v2.0.10.
The problem with ChallengeAuthenticator.isOptional() is that it's all or nothing. An alternative to the answer provided by #sea36 above is to override ChallengeAuthenticator.beforeHandle() to either perform authentication or skip it based on request method. For example, the resource below will only require authentication when the GET method is used.
Application:
package example;
import org.restlet.*;
import org.restlet.data.ChallengeScheme;
import org.restlet.routing.Router;
import org.restlet.security.ChallengeAuthenticator;
import org.restlet.security.MapVerifier;
public class ExampleApp extends Application {
private ChallengeAuthenticator createAuthenticator() {
Context context = getContext();
ChallengeScheme challengeScheme = ChallengeScheme.HTTP_BASIC;
String realm = "Example site";
// MapVerifier isn't very secure; see docs for alternatives
MapVerifier verifier = new MapVerifier();
verifier.getLocalSecrets().put("user", "password".toCharArray());
ChallengeAuthenticator authOnGet = new ChallengeAuthenticator(context, challengeScheme, realm) {
#Override
protected int beforeHandle(Request request, Response response) {
if (request.getMethod() == Method.GET)
return super.beforeHandle(request, response);
response.setStatus(Status.SUCCESS_OK);
return CONTINUE;
}
};
return authOnGet;
}
#Override
public Restlet createInboundRoot() {
ChallengeAuthenticator userResourceWithAuth = createAuthenticator();
userResourceWithAuth.setNext(UserResource.class);
Router router = new Router();
router.attach("/user", userResourceWithAuth);
return router;
}
}
Resource:
package example;
import org.restlet.resource.Get;
import org.restlet.resource.Post;
import org.restlet.representation.Representation;
import org.restlet.resource.ServerResource;
public class UserResource extends ServerResource {
#Get
public Representation listUsers() {
// retrieve list of users and generate response
// ...
}
#Post
public void register(Representation entity) {
// handle post
// ...
}
}
Note that this example applies the policy of authenticating on GET only to the UserResource and not other resources handled by the router.