- How to Execute and Disable particular testgroup or testcase in jasmine?
//Disable particular test group and case xDescribe - @ignore testgroup xit - @ignore testcase //Enable particular test group and case fdescribe - Enable Particular Test Group fit - Enable Particular Test Case
- What is Arrange-Act-Assert Pattern
- Arrange – Creating Object, Initilizing and Mocking Data
- Act – Act on your unit testcase, execute necessary functionality and methods to be unit tested
- Assert – Verifying code functionality is given output as expected
- What is difference between toBe vs toEqual?
toBe compares value where as toEqual compares object.toEqual performs deep copy comparison. - Difference between spyOn – returnValue and callFake?
If we just want a return value when a service method is called then we can use any of and.callFake or and.returnValue. But incase if the service method takes
some argument and the output of service method changes based on the argument, we use callFake with output logic in callback function. Though the same could be done
using withargs in spyOn it requires redeclaring of multiple spyOn for multiple arguments. - debugElement vs nativeElement
NativeElement provides exactlty same API methods provided by Javascript for DOM Manipulation.So whatever methods was available while working in Javascript, the same methods would be available by using instance of nativeElement. debugElement, on the other hand, is a wrapper over native element with some additional methods. Using debugElement you could access rootElement which inturn calls nativeElement to get handle of DOM object. In otherwords debugElement is again going to call nativeElement to access the DOM objects. The additional advantage of using debugElement is accessing directive and component instance which is not possible in nativeElement.
Basic of Testing – Jasmine
Before start testing method in components we initialize few things as below
- beforeEach method helps in initializing the values needed for testing
- describe are similar to JUnit class and it are similar to junit class methods which tests particular method in src code
- TestBed is needed while testing both component and html.You need to configure the TestBed before each test, adding any components, modules and services you need for the test. It’s just like configuring an regular @NgModule from scratch, but you just add what you need.
import { async, TestBed } from '@angular/core/testing'; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], providers: [], imports: [] }) .compileComponents(); }));
- Using TestBed.createComponent create component instance returns componentFixture Interface. componentFixture is a wrapper over component with additional methods needed for testing. fixture.componentInstance returns instance of component.
beforeEach(() => { . //Returns Component Fixture let fixture = TestBed.createComponent(EmployeeComponent); //Get Instance of Component component = fixture.componentInstance; . . });
- fixture.detectChanges() would allow to check for change in DOM elements before expect or whenever it is referenced for value
- DebugElement, an interface which wraps native element instead of HTML element in idle scenario
- schemas:[NO_ERRORS_SCHEMAS] lets angular testing module to ignore unknown tags in html during testing. Suppose you have used router tags in html
and angular could not recognize the router tag since routerTestingModule is not imported you can use this tag as below to avoid errors.beforeEach(() => { TestBed.configureTestingModule({ declarations: [ EmployeeComponent] schemas:[NO_ERRORS_SCHEMAS] }).compileComponents(); //Testbed to create fixture for component fixture = TestBed.createComponent(EmployeeComponent); //Using fixture to create component and service component = fixture.componentInstance; empService = TestBed.inject(EmpserviceService); });
Simple testing with matchers
- toBeTruthy tells the component is created successfully
- toBe checks for equality of value
- toEqual method does object comparison
- toContain method checks whether value is available in array
fit('Simple component testing', ()=>{ //Checks whether component has been created expect(component).toBeTruthy(); //Checks whether value returned by function are same expect(component.getGender(true)).toBe('Male'); //Checks objects are equal and array contain that element var Names = ['Name1', 'Name2', 'Name3']; expect(Names).toEqual(['Name1', 'Name2', 'Name3']); expect(Names).toContain('Name2'); });
Checking DOM Elements
- nativeElement – provides the access to DOM element.This is same as Javascript DOM Element we use for DOM Manipulation.Using nativeElement you can
access all the API methods provided vy Javascript for DOM manipulation like QuerySelector - fixture.detectChanges() – updates the DOM Element after setting the value from testing component. This is similar to refresh once the component value is set the html should be refreshed.
- debugElement– debugElement is similar to nativeElement which has wrapper with some additional methods.It has query, queryAll, queryNode methods
debugElement inturn calls nativeElement to get the values.fixture.debugElement returns the root debugElement from which we query for the nativeElement.. . fixture.debugElement.query(By.css('a')).nativeElement.textContent) fixture.debugElement.query(By.css('#ContactUsId')).nativeElement.textContent) fixture.debugElement.query(By.css('.ContactUsId')).nativeElement.textContent) . . .
- The below statement end up the same anchor tag
. fixture.debugElement.query(By.css('a')).nativeElement.textContent) fixture.nativeElement.querySelector('a').textContent .
Using jasmine – spyOn
Using spyOn with different Options
- Using spyon with callThrough would make the call to actual service
- spyOn with returnValue returns the value specified without making call to real function
- spyOn withArgs does the same but takes arguments incase there is somelogic needs to change when using along callthrough
- spyOn callFake would run anonymous function withits own logic rather than calling original function to act on logic
spyOn(empService, 'authenticateEmp').withArgs('Admin').and.callThrough(); spyOn(empService, 'authenticateEmp').and.returnValue(true); spyOn(empService, 'authenticateEmp').withArgs('Admin').and.returnValue(true); spyOn(empService, 'authenticateEmp').and.callFake(function(name, args){ return true; });
What is difference between returnValue and callFake?
If we just want a return value when a service method is called then we can use any of and.callFake or and.returnValue. Lets take below example
component file:
@Component(...) export class DependencyComponent { constructor(private service: RandomService){....} ..... sampleMethod() { return this.service.randomMethod(); } ..... }
unit test case for above component:
it('test callFake vs returnValue', () => { let randomService= new RandomService(); let component = new DependencyComponent(randomService); spyOn(randomService, 'randomMethod').and.callFake(() => 4) expect(component.sampleMethod()).toBe(4) spyOn(randomService, 'randomMethod').and.returnValue(10); expect(component.sampleMethod()).toBe(10) })
in above case both the ways are correct.
Suppose we are passing a parameter to service method to perform its logic then in that case we have to use and.callFake((param) => {…}). Here param parameter will be the argument passed to the spied method.
component file:
@Component(...) export class DependencyComponent { constructor(private service: RandomService){....} ..... sampleMethod(val) { return this.service.randomMethod(val); } ..... }
unit test case for above component:
it('test callFake vs returnValue', () => { let randomService= new RandomService(); let component = new DependencyComponent(randomService); spyOn(randomService, 'randomMethod').and.callFake((param) => param+4) expect(component.sampleMethod(4)).toBe(8); expect(component.sampleMethod(12)).toBe(16) })
when component.sampleMethod(4) is executed it will call this.service.randomMethod(4). As randomMethod() is being spied using and.callFake therefore 4 will be passed as argument of call back function of and.callFake.
One obvious question is can i use returnValue instead of callFake? You can still use but you need to define multiple returnValue methods based on total arguments needs to be tested.
Mocking Dependent Objects in Jasmine
Mocking Dependency Injection Objects
There may be times where the service would be injected into the component using constructor by DI. In such a case while creating a component instance for test class we could end up with a problem. To answer this we pass mock objects as alternate to constructors
add.employee.component.ts
export class EmployeeComponent implements OnInit { . . constructor(private objEmpserviceService : EmpserviceService) { } checkForAuthentication(): string { if(this.objEmpserviceService.authenticateEmp('Admin')){ return "You are allowed to Edit"; } return "You are Not allowed to Edit"; } . .
Two Methods to Answer this Problem
Method 1 : If you are not going to call the mockedObject methods
To address this the injected object using dependency injection we use providers. Providers provide mockedobject to constructor instead of real object like below
add.employee.component.spec.ts
let mockEmpserviceService:EmpserviceService; beforeEach(async(() => { mockEmpserviceService = jasmine.createSpyObj(['authenticateEmp', 'addEmployee', 'deleteEmployee']); TestBed.configureTestingModule({ . declarations: [ EmployeeComponent ], providers : [ {provide:EmpserviceService, useValue:mockEmpserviceService} ] . . }).compileComponents(); }));
. it('Check for Service Method', ()=>{ spyOn(component, 'checkForAuthentication'); let auth = component.checkForAuthentication(); (or) const btnAddEmp = fixture.debugElement.query(By.css("#btnAddEmp")); btnAddEmp.triggerEventHandler('click', {}); fixture.detectChanges(); expect(auth).toHaveCalled(); }); .
Method 2 : If you are going to call the mocked object methods then in such case the return type of the method should be defined.The same code can be done using actual EmpserviceService in provider and using spyOn inside its block to define the returnType of service method.
let empService: EmpserviceService; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ EmployeeComponent], providers : [ EmpserviceService], imports: [HttpClientTestingModule] }) .compileComponents(); empService = Testbed.inject(EmpserviceService); })); fit('Check for Service Method', ()=>{ //Mocking authenticateEmp to return true as if it is called spyOn(empService, 'authenticateEmp').withArgs('Admin').and.returnValue(true); spyOn(component, 'checkForAuthentication').and.callThrough(); let auth = component.checkForAuthentication(); expect(auth).toBe('You are allowed to Edit'); })
In the above code, checkForAuthentication calls authenticateEmp in empService since it does callThrough.
Note:Even now the control doesnt goes through empService. It would be called and returnvalue would be sent back.
Notes on OpenAPI 3.0
Parts of OpenAPI 3.0 Specification
Info and Components are important parts of API Definition
- Info – Provides Name, Description and Developers information of API
- Servers – Provides server information and various environment URL of API
- Security – Provides details about how API is secured, API keys, Authorization information
- Paths – Details about API Endpoints
- Tags and extenaldocs – Tags that could be grouped for API operations
- Components – Set of reusable objects which could be used during API definition
Stub vs Mockito
Below is a simple class which uses stub for Testing
- Below is a simple business implementation class which EmployeeDetailsImpl which uses EmployeeService API methods
- Now to test EmployeeDetailsImpl we need to substitude the methods in service class with stub methods for which we create EmployeeServiceStub
- EmployeeServiceStub is a stub class which implements the EmployeeService and provide method definitions
- EmployeeDetailsImplTest is the test class for EmployeeDetailsImpl which performs the unittest for methods in EmployeeDetailsImpl
EmployeeService.java
import java.util.List; public interface EmployeeService { public List<String> GetEmployeeDetails(); }
EmployeeDetailsImpl.java
import com.mugil.org.api.EmployeeService; import java.util.List; public class EmployeeDetailsImpl { EmployeeService employeeService; public EmployeeDetailsImpl(EmployeeService pEmployeeService){ this.employeeService = pEmployeeService; } public List<String> getEmployeeList(){ List<String> arrEmp = this.employeeService.GetEmployeeDetails(); return arrEmp; } }
EmployeeServiceStub.java
import java.util.Arrays; import java.util.List; public class EmployeeServiceStub implements EmployeeService{ @Override public List<String> GetEmployeeDetails() { return Arrays.asList("Mugil", "Mani", "Vinu"); } }
EmployeeDetailsImplTest.java
import com.mugil.org.api.EmployeeServiceStub; import org.junit.Test; import static org.junit.Assert.assertEquals; public class EmployeeDetailsImplTest { @Test public void getEmployeeList_success(){ EmployeeServiceStub employeeService = new EmployeeServiceStub(); EmployeeDetailsImpl employeeDetailsImpl = new EmployeeDetailsImpl(employeeService); assertEquals(3, employeeDetailsImpl.getEmployeeList().size()); } }
Dis-advantage of the above implementation
- In the above implementation when ever new method is added to the API interface, it should be added to the EmployeeServiceStub which implements EmployeeService
- EmployeeServiceStub is extra java file which should be taken care, incase there are multiple services used in class multiple service stub needs to be created
Replacing stub with mocks
In the below code, the same stub service(EmployeeServiceStub.java) is replaced with mock class and its methods are replaced using when and return.This prevents the need for another class
EmployeeDetailsImplTest.java
import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class EmployeeDetailsImplTest { @Test public void getEmployeeList_success(){ //Replacement code for Stub class EmployeeService employeeService = mock(EmployeeService.class); when(employeeService.GetEmployeeDetails()).thenReturn(Arrays.asList("Mugil", "Mani", "Vinu")); EmployeeDetailsImpl employeeDetailsImpl = new EmployeeDetailsImpl(employeeService); assertEquals(3, employeeDetailsImpl.getEmployeeList().size()); } }
Mockito Basics
In the below code we are going to mock the List Interface and override the method behaviour of List Methods
Methods mocked
- get(index)
- size()
- exception
ListTest.java
package com.mugil.org; import org.junit.Before; import org.junit.Test; import java.util.List; import static org.junit.Assert.assertNotEquals; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.junit.Assert.assertEquals; public class ListTest { List arrEmployee; @Before public void init(){ arrEmployee = mock(List.class); } @Test public void ListMock_SizeMethod(){ when(arrEmployee.size()).thenReturn(3); assertEquals(3, arrEmployee.size()); } @Test public void ListMock_GetMethod(){ when(arrEmployee.get(0)).thenReturn("Employee1"); when(arrEmployee.get(1)).thenReturn("Employee2"); when(arrEmployee.get(2)).thenReturn("Employee3"); assertEquals("Employee2", arrEmployee.get(1)); assertNotEquals(null, arrEmployee.get(2)); } @Test(expected = RuntimeException.class) public void ListMock_ThrowException(){ when(arrEmployee.get(anyInt())).thenThrow(new RuntimeException()); arrEmployee.get(1); } }
Spring Security Basics
5 Core Concepts of Spring Security
- Authentication and Authorization
– Authentication – Who are you – Answer by showing ID(Facebook, LinkedIn for ID which uniquely identifies you)
– Authorization – What you want – State what you wantKnowledge Based Authentication – Providing details you know about you to prove its you. Downside is details can be stolen.
Possession Based Authentication – Key Cards for accessing Building Doors, Phone OTP. Authenticates by checking the user posses something which
realuser should posses.Multi Factor Authentication – Enter password and enter OTP(KBA + PBA)
- Authorization – Checks whether the person is allowed to do something. For Authorization, Authentication is needed at first place.
- Principal
– Person identified through process of Authentication
– Person who has logged in. Currently logged in user (or) account.
– App remembers the principal in context as currently loggedin user. - Granted Authority
– Authority includes whether the user is allowed to Read, Write, Update and Delete at permission level - Role
– Group of Authorities assigned together forms a role
Formbased Authentication
pom.xml
. . <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> . .
Basic Auth
- Client Sends a request without username and password and gets 401 Unauthorized as Response
- Now Client Sends a request with username and password with Base64 Encoding
- Server validates whether user exists in DB
- Server replies with 200 Ok if user authentication is successful
- Basic ENCODED-BASE64-USERIDPASSWORD is the one sent in header to server from client
- In postman basic auth can be done by adding Authorization and base64 encoded user and password to header
Header : Authorization Value : Basic base64('YourOrgName:YourAPIKEY');
-
Base64 encoded text can be got from JS Console in browser as below
"username:password!" // Here I used basic Auth string format // Encode the plain string to base64 btoa("username:password!"); // output: "dXNlcm5hbWU6cGFzc3dvcmQh" // Decode the base64 to plain string atob("dXNlcm5hbWU6cGFzc3dvcmQh"); // output: "username:password!"
- Using Authorization Tab in post man does the same thing of adding base64 encoded UserName and Password to Header prepending Basic
The Difference between FormAuth and BasicAuth is in BasicAuth UserName and Password would be sent everytime when making a request to the server in the header as base64 encoded character.
Form-based authentication
Form-based authentication is not formalized by any RFC.They don’t use the formal HTTP authentication techniques.They use the standard HTML form fields to pass the username and password values to the server.The server validates the credentials and then creates a “session” that is tied to a unique key that is passed between the client and server on each http put and get request.When the user clicks “log off” or the server logs the user off (for example after certain idle time), the server will invalidate the session key, which makes any subsequent communication between the client and server require re-validation
Basic Auth
Basic Auth with Authorization in Headers as seen in DevTool
Creating the below class in Spring Boot project would enable the Basic auth(httpAuth) instead of default formbased auth which we get after adding spring security starter dependency to pom.xml
ApplicationSecurityConfig.java
Using Custom Username and Password for Inmemory Authentication
@Configuration @EnableWebSecurity public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity.authorizeRequests() .anyRequest() .authenticated() .and() .httpBasic(); } }
Spring Security Configuring Authentication and Authorization
ApplicationSecurityConfig.java
@Configuration @EnableWebSecurity public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity.authorizeRequests() .anyRequest() .authenticated() .and() .httpBasic(); } }
Whitelisting some URLs(index, js and CSS files)
@Configuration @EnableWebSecurity public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity.authorizeRequests() //Whitelisting URLS .antMatchers("/", "index", "/css/*", "/js/*").permitAll() .anyRequest() .authenticated() .and() .httpBasic(); } }
Authentication with password with no encryption
@Override @Bean protected UserDetailsService userDetailsService() { UserDetails mugilUsrBuilder = User.builder() .username("Mugil") .password("{noop}password") .roles("ADMIN") .build(); return new InMemoryUserDetailsManager(mugilUsrBuilder); }
If {noop} is not used in password Spring security would throw an error asking to encode the password with password encoder as below.
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null”
Using Password Simple Encoder
PasswordConfig.java
@Configuration public class PasswordConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(10); } }
ApplicationSecurityConfig.java
- Inject the passwordEncoder from PasswordConfig class to ApplicationSecurityConfig
- Encode the password using instance of injected encoder in ApplicationSecurityConfig
@Autowired private PasswordEncoder passwordEncoder; @Autowired public ApplicationSecurityConfig(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } @Override @Bean protected UserDetailsService userDetailsService() { UserDetails mugilUsrBuilder = User.builder() .username("Mugil") .password(this.passwordEncoder.encode("password")) .roles("ADMIN") .build(); return new InMemoryUserDetailsManager(mugilUsrBuilder); }
Allowing Access to API based on Role – Authorization
ApplicationSecurityConfig.java
@Configuration @EnableWebSecurity public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { . . @Override @Bean //Authentication protected UserDetailsService userDetailsService() { UserDetails adminUsrBuilder = User.builder() .username("admin") .password(this.passwordEncoder.encode("password")) .roles("ADMIN") .build(); UserDetails regularUsrBuilder = User.builder() .username("user") .password(this.passwordEncoder.encode("password")) .roles("USER") .build(); return new InMemoryUserDetailsManager(adminUsrBuilder, regularUsrBuilder); } @Override //Authorization protected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity.authorizeRequests() //Whitelisting URLS .antMatchers("/", "index", "/css/*", "/js/*").permitAll() .antMatchers("/api/**").hasRole("ADMIN") .anyRequest() .authenticated() .and() .httpBasic(); } . .
- In the above code we have added two roles – ADMIN and USER
- Both were authenticated to access the application.But to access the API the role should be ADMIN
antMatchers("/api/**").hasRole("ADMIN")
- If the user with Role USER try to access API then it would end up in 403 – Forbidden Error
Access Allowed
Forbidden Access
Allowing Access based on 2 Different Role
ApplicationSecurityConfig.java
@Configuration @EnableWebSecurity public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { . . @Override @Bean protected UserDetailsService userDetailsService() { UserDetails adminUsrBuilder = User.builder() .username("admin") .password(this.passwordEncoder.encode("password")) .roles("ADMIN") .build(); UserDetails regularUsrBuilder = User.builder() .username("user") .password(this.passwordEncoder.encode("password")) .roles("USER") .build(); return new InMemoryUserDetailsManager(adminUsrBuilder, regularUsrBuilder); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity.csrf().disable() .authorizeRequests() //Whitelisting URLS .antMatchers("/", "index", "/css/*", "/js/*").permitAll() .antMatchers(HttpMethod.GET,"/api/**").permitAll() .antMatchers(HttpMethod.DELETE,"/api/**").hasRole("ADMIN") .antMatchers(HttpMethod.PUT,"/api/**").hasRole("ADMIN") .antMatchers(HttpMethod.POST,"/api/**").hasRole("ADMIN") .anyRequest() .authenticated() .and() .httpBasic(); } }
- In the above piece of code we have defined two roles ADMIN and USER
- Those with USER role can access the API with HTTP Get Method. That means both ADMIN and USER role could access all the API using GET method
. .antMatchers(HttpMethod.GET,"/api/**").permitAll() .
-
Those with ADMIN role can access the API with HTTP POST, DELETE and PUT Method which corresponds to Create, Delete and Update as per Open API Specifiaction.
. .antMatchers(HttpMethod.DELETE,"/api/**").hasRole("ADMIN") .antMatchers(HttpMethod.PUT,"/api/**").hasRole("ADMIN") .antMatchers(HttpMethod.POST,"/api/**").hasRole("ADMIN") .
- The above could be cross checked by changing postman call with HttpMethods and Credentials
StudentsService.java
@RestController @RequestMapping("/api/v1/students") public class StudentsService { @Autowired StudentRepo studentRepo; @GetMapping(path="{studentId}") public Student getStudentById(@PathVariable("studentId") String studentId){ return studentRepo.getStudentById(studentId); } @GetMapping public List<Student> getStudentList(){ return studentRepo.getStudentsList(); } @PutMapping public String updateStudent(@RequestBody Student student){ return studentRepo.updateStudent(student); } @PostMapping public String addStudent(@RequestBody Student student){ if(studentRepo.addStudents(student)) return "Student with Id " + student.getStudentId() + " added successfully"; else return "Error:Unable to create Student"; } @DeleteMapping(path="{studentId}") public String deleteStudent(@PathVariable("studentId") String studentId){ studentRepo.deleteStudent(studentId); return "Student Deleted Successfully"; } }
Allowing Access based on 2 Different Authority(or)Permission
- In the below code instead of using ROLES to authorize users to do something we use AUTHORITIES to allow user
- There are two ways to do this. One is by using hasAuthority in configure(HttpSecurity httpSecurity) method as below
-
. @EnableGlobalMethodSecurity(prePostEnabled = true) . .antMatchers(HttpMethod.POST,"/api/v1/students/").hasAuthority("WRITE") .antMatchers(HttpMethod.DELETE,"/api/v1/students/**").hasAuthority("WRITE") .antMatchers(HttpMethod.PUT,"/api/v1/students/").hasAuthority("WRITE") .
- Other is by using @preauthorize annotation to decide the methods
which could be allowed access to. . @PreAuthorize("hasAuthority('READ')") public List
getStudentList(){ . @PreAuthorize("hasAuthority('WRITE')") public String updateStudent(@RequestBody Student student){ . .
ApplicationSecurityConfig.java
Using hasAuthority
@Configuration @EnableWebSecurity public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Autowired public ApplicationSecurityConfig(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } @Override @Bean protected UserDetailsService userDetailsService() { GrantedAuthority[] arrGrantedAuthAdmin = {new SimpleGrantedAuthority("READ"), new SimpleGrantedAuthority("WRITE")}; GrantedAuthority[] arrGrantedAuthUser = {new SimpleGrantedAuthority("READ")}; UserDetails adminUsrBuilder = User.builder() .username("admin") .password(this.passwordEncoder.encode("password")) .authorities("READ", "WRITE") .build(); UserDetails regularUsrBuilder = User.builder() .username("user") .password(this.passwordEncoder.encode("password")) .authorities("READ") .build(); return new InMemoryUserDetailsManager(adminUsrBuilder, regularUsrBuilder); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity.csrf().disable() .authorizeRequests() .antMatchers("/", "index", "/css/*", "/js/*").permitAll() .antMatchers(HttpMethod.POST,"/api/v1/students/").hasAuthority("WRITE") .antMatchers(HttpMethod.DELETE,"/api/v1/students/**").hasAuthority("WRITE") .antMatchers(HttpMethod.PUT,"/api/v1/students/").hasAuthority("WRITE") .anyRequest() .authenticated() .and() .httpBasic(); } }
ApplicationSecurityConfig.java
Using @PreAuthorize and EnableGlobalMethodSecurity
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Autowired public ApplicationSecurityConfig(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } @Override @Bean protected UserDetailsService userDetailsService() { GrantedAuthority[] arrGrantedAuthAdmin = {new SimpleGrantedAuthority("READ"), new SimpleGrantedAuthority("WRITE")}; GrantedAuthority[] arrGrantedAuthUser = {new SimpleGrantedAuthority("READ")}; UserDetails adminUsrBuilder = User.builder() .username("admin") .password(this.passwordEncoder.encode("password")) .authorities("READ", "WRITE") .build(); UserDetails regularUsrBuilder = User.builder() .username("user") .password(this.passwordEncoder.encode("password")) .authorities("READ") .build(); return new InMemoryUserDetailsManager(adminUsrBuilder, regularUsrBuilder); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity.csrf().disable() .authorizeRequests() .antMatchers("/", "index", "/css/*", "/js/*").permitAll() .anyRequest() .authenticated() .and() .httpBasic(); } }
StudentsService.java
@RestController @RequestMapping("/api/v1/students") public class StudentsService { @Autowired StudentRepo studentRepo; @GetMapping(path="{studentId}") public Student getStudentById(@PathVariable("studentId") String studentId){ return studentRepo.getStudentById(studentId); } @GetMapping @PreAuthorize("hasAuthority('READ')") public List<Student> getStudentList(){ return studentRepo.getStudentsList(); } @PutMapping @PreAuthorize("hasAuthority('WRITE')") public String updateStudent(@RequestBody Student student){ return studentRepo.updateStudent(student); } @PostMapping @PreAuthorize("hasAuthority('WRITE')") public String addStudent(@RequestBody Student student){ if(studentRepo.addStudents(student)) return "Student with Id " + student.getStudentId() + " added successfully"; else return "Error:Unable to create Student"; } @DeleteMapping(path="{studentId}") @PreAuthorize("hasAuthority('WRITE')") public String deleteStudent(@PathVariable("studentId") String studentId){ studentRepo.deleteStudent(studentId); return "Student Deleted Successfully"; } }
How CSRF works in Spring Security
- CSRF Refers to Cross Site Request Forgery. More on CSRF Here
- When the Client makes the first GET request to the Server, Server generates CSRF token and sends back to Client
- On the Subsequent PUT,POST and DELETE request from the Client this token would be used for Authentication
- In Postman for the Same reason GET request would work irrespect we disable the CSRF in protected void configure(HttpSecurity httpSecurity) method. But for the PUT,POST and DELETE we should disable the CSRF as below otherwise the server would expect CSRF token when the request to server is PUT,POST and DELETE.
@Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf().disable() .authorizeRequests() .antMatchers("/", "index", "/css/*", "/js/*").permitAll() . .
Using CSRF Token for Authorization
- We have set of API’s exposed to the users whose role is ADMIN
- The way CSRF works is first Token would be generated once the User Logins using UserID and Password. The Way it works in REST API is first time
when you use GET method you should use Basic Auth in Authorization to Generate XSRF-TOKEN which would be set in Cookie along with JSessionID and also available in Response Headers - By default CSRF is not applicable for GET unless it is pointed to resource API. Simple reason behind that is, when the user types URL of login thats the first page which take login. If you try to access http://localhost:8080/ in postman it works fine without CSRF but when you access http://localhost:8080/api/v1/students/ which again uses GET method but points to resource API then it asks for UserId and Password due to basic Auth
- Subsequent POST, PUT and DELETE request could be done by attaching X-XSRF-TOKEN to header. The Cookie which has the JSESSIONID and XSRF-TOKEN should not be deleted in Postman
- Though CSRF is enabled explicity in my Code it didnt worked until i added following lines in ApplicationSecurityConfig.java
. . httpSecurity .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .
Accessing resource using GET (No Authentication or Authorization)
Accessing resource using GET (Authentication needed for API Resource)
Accessing resource using POST (X-XSRF-TOKEN) in request header
XSRF-TOKEN received after GET with credentials and X-XSRF-TOKEN in haeder for sunsequent POST,PUT and DELETE calls
ApplicationSecurityConfig.java
Using CSRF Token for Authorization
@Override protected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .authorizeRequests() .antMatchers("/", "index", "/css/*", "/js/*").permitAll() .antMatchers("/api/**").hasRole("ADMIN") .anyRequest() .authenticated() .and() .httpBasic(); }
FAQ
There are N number of possibilities. Try out in Postman.
- How GET method to API resource Works with out CSRF token?
It works based on JSESSIONID. When the JSESSIONID in cookie is deleted then again it asks for Login and password - What happens when I delete XSRF token in Cookie?
New Token would be generated based on JSESSIONID in cookie - What happens when I delete XSRF token and in Cookie and try POST, DELETE and PUT over API?
New JSESSIONID would be generated and placed in cookie. For this X-XSRF-TOKEN should be passed in header.