- Spring-Security
- Section 1 Getting Started
- Section 2 Default Security Configurations
- Section 3 Defining and Managing Users
- 22 Approach 1 Configuring Users Using InMemoryUserDetailsManager
- 23 Approach 2 Configuring Users Using inMemoryUserDetailsManager
- 24 Understanding User Management Interfaces and Classes
- 25 Deep Dive of UserDetails Interface and User Class
- 26 Deep Dive of UserDetailsService and UserDetailsManager
- 27 Deep Dive of UserDetailsManager implementation classes
- 28 Creating MySQL DB
- 29 Connecting to DB
- 30 Using JdbcUserDetailsManager
- 31 Creating Custom Authentication Tables
- 32 Creating JPA Entity and Repo For New Table
- 33 Create Custom Implementation of UserDetailsService
- 34 Allowing New User Registration
- Update Sequence With Custom JBDC User Details Service
- Section 4 Password Management and Encoders
- 35 How Are Passwords Validated In Spring Security By Default
- 36 Encoding vs Encryption vs Hashing Part 1
- 37 Encoding vs Encryption vs Hashing Part 2
- 38 How to Validate Passwords With Hashing and Password Encoders
- 39 PasswordEncoder Interface
- 40 Password Encoder Implementation Classes part 1
- 41 Password Encoder Implementation Classes part 2
- 42 Register New User With ByCrypt Password Encoder
- 43 Login With ByCrypt Password Encoder
- Section 5 Authentication Providers
- Section 6 Cors and Csrf
- 49 Setting up Separate UI
- 51 Creating new DB for schema
- 53 creating A New User With Postman
- 54 Cors Error
- 55 Introduction to Cors
- 56 Options to Fix Cors
- 57 Fixing Cors Using Spring Security
- 58 Demo of CSRF Protection from Spring Security
- 59 Example CSRF Attack
- 60 Solutions for CSRF Attacks
- 61 Ignoring CSRF Protection For Public API endpoints
- 62 Implementing CSRF Token Solution
- Section 7 Authorization
- 64 Authentication vs Authorization
- 65 How Authorities Are Stored In Spring Security
- 66 Creating New Table Authorities
- 67 Backend Changes to Load Authorities
- 68 and 69 Configure Authorities Inside App Using Spring Security
- 70 Authority vs Role in Spring Security
- 71 and 72 Configuring Roles Authorization
- Section 8 Custom Spring Filters
- Section 9 Authentication With JWT
- Section 10 Method Level Security
- Section 11 Deep Dive Into Oauth2 and OpenID Connect
- 98 Problem OAuth2 is Trying to Solve
- 99 Introduction to OAuth2
- 100 OAuth2 Terminology
- 101 OAuth2 Sample Flow
- 103 Deep Dive and Demo Authorization Code Grant Type Flow in OAuth2
- 105 Deep Dive and Demo Implicit Grant Flow in OAuth2
- 106 Deep Dive of Password Grant Type Flow in OAuth2
- 107 Deep Dive of Credentials Grant Type Flow in OAuth2
- 108 Deep Dive of Refresh Token Grant Type Flow in OAuth2
- 109 How Resource Server Validates Tokens Issue by the Auth Server
- 110 Introduction to OpenID Connect
- Section 12 Implementing OAuth2 Using Spring Security
- Section 13 Implementing OAuth2 with Keycloak
- 114 OAuth2 in EazyBank App
- 115 Introduction to Keycloak Auth Server
- 116 Installing Keycloak and Setting Up Admin Account
- 117 Creating KeyCloak Realms
- 118 Creating Client Credentials
- 119 Setting Up EazyBank Resource Server
- 120 Getting Access Token From Keycloak with Client Credentials Grant Type
- 121 Passing Access Token to Resource Server Through Postman
- Authorization Code Grant Type
- 122 Authrorization Code Grant Type with EazyBank
- 123 Creating Client and User Details in KeyCloak
- 124 Testing Authorization Code Grant Type with Postman
- 125 126 Authorization Code with PKCE
- 127 Creating Public Facing Client Details in Keycloak Server
- 128 Implementing PKCE Authorization Code Grant Type in Angular UI Part
- 131 Important KeyCloak Features
- 132 Social Login Integration with Keycloak Server
For spring security zero to master
can create a spring project at https://start.spring.io/
- most basic project uses spring web (web server default is tomcat), and dev tools
- can add in spring security for security starter
Diff with vs without spring security
- for slf4j logging annotation
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
and exclude from the build
build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
- as soon as you add the spring security dependency, without doing anything it will start intercepting calls
- will show a login screen, user:
user
, password is printed in the console
common application properties yaml vs properties files
spring.security.user.name=myuser
spring.security.user.password=testing
- app security is very complex
- always new exploits, frameworks update and patch known vulnerabilities
- spring security is built by security experts, allowing us to secure our app for with very minimal configuration
- open source
- protects against common exploits
- Supports RBAC
- Supports various types of auth, JWT, OAuth2, OpenID, Username and Password etc...
- servlet is created internally by spring
- filter is used to intercept every request to the application
- acts a middleware in between client and servlet/business logic
- user enters credentials
- Filters create authentication object such as the
UsernamePasswordAuthenticationToken
- Authentication object handed to the authentication manager
- Authentication Manager, which is implemented by
ProviderManager
checks available authentication provider - Authentication Providers decide if user is valid
- can be many authentication providers
- can write logic in authentication providers to decide how to authenticate
- Authentication manager will try all authentication providers, not just one
- Can leverage spring security classes
UserDetailsManager
andUserDetailsService
- Whatever
PasswordEncoder
bean encodes passwords as to not store in plain text
- works with
UserDetailsManager/Service
to decide if the user should be authenticated
- Sends response back to authentication manager
- Forwards back to security filters
- With an update Authentication Object, the filters store it in the security context
- store authentication status, session id etc...
- this is why user doesn't have to log in again
- Final yes/no response sent back to end user
@ComponentScan("com.eazybytes.sprinsecuritybasic.controller")
-
sprinsecuritybasic/src/main/java/com/eazybytes/sprinsecuritybasic/SprinsecuritybasicApplication.java
-
optional in this setup as spring scans components in THIS and sub packages, however if we split our app out into a package outside springsecruitybasic then would need to annotate
- spring security protects all paths by default
- this is configured in the
defaultSecurityFilterChain
bean
-
http.authorizeRequests().anyRequest().authenticated();
- says authorize any request made to the backend -
from the comments
The default configuration for web security. It relies on Spring Security's content-negotiation strategy to determine what sort of authentication to use. ** If the user specifies their own SecurityFilterChain bean, this will back-off completely and the users should specify all the bits that they want to configure as part of the custom security configuration **.
- if we create a bean of securityFilter chain then our custom auth will be taking place
-
can permit all or choose which paths need authentication
-
see ./02_section/sprinsecuritysec2/src/main/java/com/eazybytes/config/ProjectSecurityConfig.java
- could deny all if they want
- could do this to either test security, go through updates or turn off the services for a period of time
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().anyRequest().denyAll();
http.formLogin();
http.httpBasic();
return http.build();
}
- could allow all requests
- could be done for development environments via conditional beans
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().anyRequest().permitAll();
http.formLogin();
http.httpBasic();
return http.build();
}
- not for production
- can define multiple users along with their authorities with the help fo
InMemoryUserDetailsManager
andUserDetails
- use the
withDefaultPasswordEncoder()
method, this method is deprecated
- not for production
- here we create a bean of
NoOpPasswordEncoder
, which implementsPasswordEncoder
, this bean will automatically be picked up by spring
- the
DaoAuthenticationProvider
that comes with spring will look to user theinMemoryUserDetailsManager
,JdbcUserDetailsManager
andLdapUserDetailsManager
- the details managers are sample implementations provided by spring security
- The
UserDetails
interface allows us to represent the User as aUser
class that implements theUserDetails
- When you return a new class of type
...UserDetailsManager
, theloadUserByUsername
method is called- this method returns
UserDetails
which implementsUserDetailsManager
which extendsUserDetailsService
- this method returns
-
holds methods for
- getAuthorities()
- getPassword()
- getUsername()
- isAccountNonExpired()
- isAccountNonLocked()
- isCredentialsNonExpired()
- isEnabled()
-
sample of this interface are the
User
Class, we can use this or implement our ownuserDetails
-
this object is readOnly, there are no setters, once the object is created through the constructor it is immutable
- identify if authentication is successful or not inside the authentication providers
AuthenticationProvider
will convert the userDetails into the Authentication Token, after fetched from the database and authenticated- this is done by default in the
AbstractUserDetailsAuthenticationProvider.java
,authenticate
method, if authenticated then it called thecreateSuccessAuthentication
method witch takes in an auth token, user and returns back a populated authentication token
UserDetailsService
, holds the methodloadUserByUsername
which loads the user from the database- only username is loaded, not the password, which we dont want to move over the network
UserDetailsService
is extended by theUserDetailsManager
which gives the ability to perform CRUD operations on users- offers a userExists() method that tells if that user already exists in the system
inMemoryUserDetailsManager
,JdbcUserDetailsManager
andLdapUserDetailsManager
are the most commonly used and examples are provided by spring
- for
InMemoryUserDetailsManager
thecreateUser()
method is called through the constructor - holds the
loadUserByUsername()
method DaoAuthenticationProvider
knows which details manager to invoke by which beans are createdInMemoryUserDetailsManager
used mostly for dev and demo
- most common for production
- spring security assumes a specific table structure, they have programmed queries for this configuration
- table name must be
users
- there is a default schema included in the class, pointing to a ddl file.
- can use the
users.ddl
file to create the database architecture
- interface
- implemented by
JdbcUserDetailsManager
- helps create and add users to groups for RBAC
- not as common
- still has
loadUserByUserName
- have to have an LDAP server configured
- add following dependencies
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
- can just do this in docker
docker pull mysql
docker run --name spring-security-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=pwd -e useSSL=false -d mysql
- host = localhost
- username = root
- pwd = pwd
- port = 3306
- go to driver properties -> set
allowPublicKeyRetrieval
to true - https://stackoverflow.com/questions/61749304/connection-between-dbeaver-mysql
- have to create and seed the database
create database eazybank;
use eazybank;
CREATE TABLE `users` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45) NOT NULL,
`password` VARCHAR(45) NOT NULL,
`enabled` INT NOT NULL,
PRIMARY KEY (`id`));
CREATE TABLE `authorities` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(45) NOT NULL,
`authority` varchar(45) NOT NULL,
PRIMARY KEY (`id`));
INSERT IGNORE INTO `users` VALUES (NULL, 'happy', '12345', '1');
INSERT IGNORE INTO `authorities` VALUES (NULL, 'happy', 'write');
CREATE TABLE `customer` (
`id` int NOT NULL AUTO_INCREMENT,
`email` varchar(45) NOT NULL,
`pwd` varchar(200) NOT NULL,
`role` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `customer` (`email`, `pwd`, `role`)
VALUES ('t@t.com', '54321', 'admin');
- can be used for small or flexible projects but example tables might not be usable for everyone
- have to add dependencies for spring jdbc, mysql and spring data jpa
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
- connect with
# dont inject like this in production, should come from devops team
spring.datasource.url=jdbc:mysql://localhost/eazybank
spring.datasource.username=root
spring.datasource.password=pwd
#print sql in the console, not for production
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
- will then create the JdbcUserDetailsManager
@Bean
public UserDetailsService userDetailsService(DataSource dataSource){
return new JdbcUserDetailsManager(dataSource);
}
-
Spring boot creates an object of type data source in memory when we add jbdc information to class path and application properties
-
can have multiple
UserDetailsManagers
if have multipleAuthenticationProviders
- in schema above
- will have to create your own JPA entity, wont be able to sue the default one
-
have to create a repository
-
sprinsecuritysec3/src/main/java/com/eazybytes/repository/CustomerRepository.java
-
have to create a model
-
sprinsecuritysec3/src/main/java/com/eazybytes/model/Customer.java
- have to add 2 annotations to spring application if repositories or entities are not in the main package
- will scan and create beans for the packages
@EnableJpaRepositories("com.eazybytes.repository")
@EntityScan("com.eazybytes.model")
- if not using the spring security starter must have annotation to turn on web security
@EnableWebSecurity
-
if we are using our own database setup, we must override the default
UserDetailsService
and write logic for loading the user in theloadUserByUsername
method -
sprinsecuritysec3/src/main/java/com/eazybytes/config/EazyBankUserDetails.java
- if you have
userDetailsService
it will confuse the defaultDaoAuthenticationProvider
, can have multiple details services but have to have customAuthenticationProvider
- Could override the
UserDetailsManager
createUser()
method - see for example sprinsecuritysec3/src/main/java/com/eazybytes/controller/LoginController.java
- have to permit non authenticated users to hit the /register path
- this point can create with postman
{
"email": "1@1.com",
"pwd": "12345",
"role": "user"
}
- successful response will be
Given user details are successfully registered
-
AbstractUserDetailsAuthenticationProvider
-authenticate()
method -
runs some
preAuthenticationChecks()
such as checking for disabled, expired etc -
then runs
DaoAuthenticationProvider
-additionalAuthenticationChecks()
additionalAuthenticationChecks()
calls thematches()
method defined in every password encodermatches
takes in the password the user typed and the password loaded from the database- if passwords match you are authenticated
- if not
BadCredentialsException
is thrown
- default password encoder
- doesn't actually encode anything
- stores passwords in plain text
- not for production
- password encoders take care of comparing hash strings
- includes:
- encode - will put password through corresponding hash/encryption (even it that is none)
- matches (true if the same, ie good login)
- upgradeEncoding - can encode your password two times, default is false
- not for production
- default password encoder
- doesn't actually encode anything
- stores passwords in plain text
- not for production, only demo/dev
- not for production
- deprecated
- only implemented to support legacy applications
- sha-256, 1024 iterations and random 8-byte salt
- can use in production but not recommended
- has become less secure over last few years
- susceptible to brute force attacks
- can use in production
- should have strong password requirements, ie: 8 Chars, letters, numbers and special characters
- strong password requirements make brute force nearly impossible with any of the following 3 passwordEncoders
- uses Bcrypt hashing algorithm
- continually updated based on hardware and best practices
- utilizes more cpu/resources
- less susceptible to brute force
- secure
- most commonly used
- advance version of
BCryptPasswordEncoder
- uses both cpu and ram
- makes brute force attacks more difficult due to resource restriction
- more secure
- newest based on latest hashing algorithm
- uses both cpu, ram and multiple threads
- make brute force tougher due to resource restriction
- also can slow down our application due to resource needs
- most secure
- create a passwordEncoder bean with the
BCryptPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
-
passwords are not hashed when the creating an new user ex: 04_section/sprinsecuritysec4/src/main/java/com/eazybytes/controller/LoginController.java
-
by default does 10 rounds of hashing but this can be configured
-
could change options if wanted to utilizing different constructors of
BCryptPasswordEncoder
-
min rounds is 4, max is 31, default is 10
-
could pass a salt if we had one
-
from javadoc of
BCryptPasswordEncoder
Implementation of PasswordEncoder that uses the BCrypt strong hashing function. Clients can optionally supply a "version" ($2a, $2b, $2y) and a "strength" (a.k.a. log rounds in BCrypt) and a SecureRandom instance. The larger the strength parameter the more work will have to be done (exponentially) to hash the passwords. The default value is 10.
- see difference ins stored passwords
-
plain text passwords will no longer with when
BCryptPasswordEncoder
is enabled -
BCryptPasswordEncoder
knows this is not a BCrypt encoded password -
will give error in console
2023-01-03T17:37:21.975-05:00 WARN 54197 --- [nio-8888-exec-7] o.s.s.c.bcrypt.BCryptPasswordEncoder : Encoded password does not look like BCrypt
-
if using the default
DaoAuthenticationProvider
then no additional configuration is needed.DaoAuthenticationProvider
will pick up theBCryptPasswordEncoder
bean and handle calling functions for password comparison -
could define your own password encoder by implementing PasswordEncoder interface but not recommended
- the default authentication provider is
DaoAuthenticationProvider
- this is a flexible class that allows us to change
UserDetailsService
as well asPasswordEncoder
, however it does not support all use cases - we may want to create our own authentication logic ie: only allowed age or countries
- may want multiple authentication providers
-
authenticate()
method actually kicks off the authenticate process- returns an authentication token with a user that is either authenticated or not
-
supports()
- tells which type of authentication objects are supported- called inside the
ProviderManager
, which implements theAuthenticationManager
interface, to check if the current authenticationProvider supports the type of authenticationToken that is passed in
- called inside the
-
DaoAuthenticationProvider
supportsUsernamePasswordAuthenticationToken
out of the box -
other auth tokens provided by spring
TestingAuthenticationToken
AnonymousAuthenticationToken
RememberMeAuthenticationToken
-
since we created our own custom
AuthenticationProvider
, we no longer need auserDetailsService
. If wa wanted to make our customAuthenticationProvider
depend on auserDetailsService
, like the defaultDaoAuthenticationProvider
then we could use on, but not necessary as this logic is now in ourEazyBankUsernamePwdAuthenticationProvider
, which is talking directly to the CrudRepository via DI -
example : sprinsecuritysec5/src/main/java/com/eazybytes/config/EazyBankUsernamePwdAuthenticationProvider.java
EazyBankUsernamePwdAuthenticationProvider.authenticate()
is called by the spring securityProviderManager
- after authentication is completed based on logic in the
authenticate()
method theProviderManager
does some cleanup including deleting the credentials we were comparing against - can also have an event published (an use this to send a push notification/email)
Provider Manager
implements AuthenticationManager
, they are interchangeable
-
SpringSecurityFilter and
AuthenticationManager
do their own job, we do not override those -
we changed the authenticationProvider and its methods (which are called by the
AuthenticationManager
), incorporating the database call into our customEazyBankUsernamePwdAuthenticationProvider
. Since we were no longer using the defaultDaoAuthenticationProvider
, and incorporated theCustomerRepository
logic, we did not need aUserDetailsService
/UserDetailsManager
-
Also overrode the
PasswordEncoder
to use theBCryptPasswordEncoder
- Starting in this section we will start using the Angular UI to talk to our backend
- directions for setting up are ./angular_ui/bank-app-ui/README.md
- run the following schema
use eazybank;
drop table `users`;
drop table `authorities`;
drop table `customer`;
drop table accounts;
CREATE TABLE `customer` (
`customer_id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`email` varchar(100) NOT NULL,
`mobile_number` varchar(20) NOT NULL,
`pwd` varchar(500) NOT NULL,
`role` varchar(100) NOT NULL,
`create_dt` date DEFAULT NULL,
PRIMARY KEY (`customer_id`)
);
INSERT INTO `customer` (`name`,`email`,`mobile_number`, `pwd`, `role`,`create_dt`)
VALUES ('Happy','happy@example.com','9876548337', '$2y$12$oRRbkNfwuR8ug4MlzH5FOeui.//1mkd.RsOAJMbykTSupVy.x/vb2', 'admin',CURDATE());
CREATE TABLE `accounts` (
`customer_id` int NOT NULL,
`account_number` int NOT NULL,
`account_type` varchar(100) NOT NULL,
`branch_address` varchar(200) NOT NULL,
`create_dt` date DEFAULT NULL,
PRIMARY KEY (`account_number`),
KEY `customer_id` (`customer_id`),
CONSTRAINT `customer_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`customer_id`) ON DELETE CASCADE
);
INSERT INTO `accounts` (`customer_id`, `account_number`, `account_type`, `branch_address`, `create_dt`)
VALUES (1, 18657645, 'Savings', '123 Main Street, New York', CURDATE());
CREATE TABLE `account_transactions` (
`transaction_id` varchar(200) NOT NULL,
`account_number` int NOT NULL,
`customer_id` int NOT NULL,
`transaction_dt` date NOT NULL,
`transaction_summary` varchar(200) NOT NULL,
`transaction_type` varchar(100) NOT NULL,
`transaction_amt` int NOT NULL,
`closing_balance` int NOT NULL,
`create_dt` date DEFAULT NULL,
PRIMARY KEY (`transaction_id`),
KEY `customer_id` (`customer_id`),
KEY `account_number` (`account_number`),
CONSTRAINT `accounts_ibfk_2` FOREIGN KEY (`account_number`) REFERENCES `accounts` (`account_number`) ON DELETE CASCADE,
CONSTRAINT `acct_user_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`customer_id`) ON DELETE CASCADE
);
INSERT INTO `account_transactions` (`transaction_id`, `account_number`, `customer_id`, `transaction_dt`, `transaction_summary`, `transaction_type`,`transaction_amt`,
`closing_balance`, `create_dt`) VALUES (UUID(), 18657645, 1, "2022-12-24", 'Coffee Shop', 'Withdrawal', 30,34500, "2022-12-24");
INSERT INTO `account_transactions` (`transaction_id`, `account_number`, `customer_id`, `transaction_dt`, `transaction_summary`, `transaction_type`,`transaction_amt`,
`closing_balance`, `create_dt`) VALUES (UUID(), 18657645, 1, "2022-12-25", 'Uber', 'Withdrawal', 100,34400,"2022-12-25");
INSERT INTO `account_transactions` (`transaction_id`, `account_number`, `customer_id`, `transaction_dt`, `transaction_summary`, `transaction_type`,`transaction_amt`,
`closing_balance`, `create_dt`) VALUES (UUID(), 18657645, 1, "2022-12-26", 'Self Deposit', 'Deposit', 500,34900,"2022-12-26");
INSERT INTO `account_transactions` (`transaction_id`, `account_number`, `customer_id`, `transaction_dt`, `transaction_summary`, `transaction_type`,`transaction_amt`,
`closing_balance`, `create_dt`) VALUES (UUID(), 18657645, 1, "2022-12-27", 'Ebay', 'Withdrawal', 600,34300,"2022-12-27");
INSERT INTO `account_transactions` (`transaction_id`, `account_number`, `customer_id`, `transaction_dt`, `transaction_summary`, `transaction_type`,`transaction_amt`,
`closing_balance`, `create_dt`) VALUES (UUID(), 18657645, 1, "2022-12-28", 'OnlineTransfer', 'Deposit', 700,35000,"2022-12-28");
INSERT INTO `account_transactions` (`transaction_id`, `account_number`, `customer_id`, `transaction_dt`, `transaction_summary`, `transaction_type`,`transaction_amt`,
`closing_balance`, `create_dt`) VALUES (UUID(), 18657645, 1, "2022-12-29", 'Amazon.com', 'Withdrawal', 100,34900,"2022-12-29");
CREATE TABLE `loans` (
`loan_number` int NOT NULL AUTO_INCREMENT,
`customer_id` int NOT NULL,
`start_dt` date NOT NULL,
`loan_type` varchar(100) NOT NULL,
`total_loan` int NOT NULL,
`amount_paid` int NOT NULL,
`outstanding_amount` int NOT NULL,
`create_dt` date DEFAULT NULL,
PRIMARY KEY (`loan_number`),
KEY `customer_id` (`customer_id`),
CONSTRAINT `loan_customer_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`customer_id`) ON DELETE CASCADE
);
INSERT INTO `loans` ( `customer_id`, `start_dt`, `loan_type`, `total_loan`, `amount_paid`, `outstanding_amount`, `create_dt`)
VALUES ( 1, '2020-10-13', 'Home', 200000, 50000, 150000, '2020-10-13');
INSERT INTO `loans` ( `customer_id`, `start_dt`, `loan_type`, `total_loan`, `amount_paid`, `outstanding_amount`, `create_dt`)
VALUES ( 1, '2020-06-06', 'Vehicle', 40000, 10000, 30000, '2020-06-06');
INSERT INTO `loans` ( `customer_id`, `start_dt`, `loan_type`, `total_loan`, `amount_paid`, `outstanding_amount`, `create_dt`)
VALUES ( 1, '2018-02-14', 'Home', 50000, 10000, 40000, '2018-02-14');
INSERT INTO `loans` ( `customer_id`, `start_dt`, `loan_type`, `total_loan`, `amount_paid`, `outstanding_amount`, `create_dt`)
VALUES ( 1, '2018-02-14', 'Personal', 10000, 3500, 6500, '2018-02-14');
CREATE TABLE `cards` (
`card_id` int NOT NULL AUTO_INCREMENT,
`card_number` varchar(100) NOT NULL,
`customer_id` int NOT NULL,
`card_type` varchar(100) NOT NULL,
`total_limit` int NOT NULL,
`amount_used` int NOT NULL,
`available_amount` int NOT NULL,
`create_dt` date DEFAULT NULL,
PRIMARY KEY (`card_id`),
KEY `customer_id` (`customer_id`),
CONSTRAINT `card_customer_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`customer_id`) ON DELETE CASCADE
);
INSERT INTO `cards` (`card_number`, `customer_id`, `card_type`, `total_limit`, `amount_used`, `available_amount`, `create_dt`)
VALUES ('4565XXXX4656', 1, 'Credit', 10000, 500, 9500, "2022-12-31");
INSERT INTO `cards` (`card_number`, `customer_id`, `card_type`, `total_limit`, `amount_used`, `available_amount`, `create_dt`)
VALUES ('3455XXXX8673', 1, 'Credit', 7500, 600, 6900, "2022-12-31");
INSERT INTO `cards` (`card_number`, `customer_id`, `card_type`, `total_limit`, `amount_used`, `available_amount`, `create_dt`)
VALUES ('2359XXXX9346', 1, 'Credit', 20000, 4000, 16000, "2022-12-31");
CREATE TABLE `notice_details` (
`notice_id` int NOT NULL AUTO_INCREMENT,
`notice_summary` varchar(200) NOT NULL,
`notice_details` varchar(500) NOT NULL,
`notic_beg_dt` date NOT NULL,
`notic_end_dt` date DEFAULT NULL,
`create_dt` date DEFAULT NULL,
`update_dt` date DEFAULT NULL,
PRIMARY KEY (`notice_id`)
);
INSERT INTO `notice_details` ( `notice_summary`, `notice_details`, `notic_beg_dt`, `notic_end_dt`, `create_dt`, `update_dt`)
VALUES ('Home Loan Interest rates reduced', 'Home loan interest rates are reduced as per the goverment guidelines. The updated rates will be effective immediately',
CURDATE() - INTERVAL 30 DAY, CURDATE() + INTERVAL 30 DAY, CURDATE(), null);
INSERT INTO `notice_details` ( `notice_summary`, `notice_details`, `notic_beg_dt`, `notic_end_dt`, `create_dt`, `update_dt`)
VALUES ('Net Banking Offers', 'Customers who will opt for Internet banking while opening a saving account will get a $50 amazon voucher',
CURDATE() - INTERVAL 30 DAY, CURDATE() + INTERVAL 30 DAY, CURDATE(), null);
INSERT INTO `notice_details` ( `notice_summary`, `notice_details`, `notic_beg_dt`, `notic_end_dt`, `create_dt`, `update_dt`)
VALUES ('Mobile App Downtime', 'The mobile application of the EazyBank will be down from 2AM-5AM on 12/05/2020 due to maintenance activities',
CURDATE() - INTERVAL 30 DAY, CURDATE() + INTERVAL 30 DAY, CURDATE(), null);
INSERT INTO `notice_details` ( `notice_summary`, `notice_details`, `notic_beg_dt`, `notic_end_dt`, `create_dt`, `update_dt`)
VALUES ('E Auction notice', 'There will be a e-auction on 12/08/2020 on the Bank website for all the stubborn arrears.Interested parties can participate in the e-auction',
CURDATE() - INTERVAL 30 DAY, CURDATE() + INTERVAL 30 DAY, CURDATE(), null);
INSERT INTO `notice_details` ( `notice_summary`, `notice_details`, `notic_beg_dt`, `notic_end_dt`, `create_dt`, `update_dt`)
VALUES ('Launch of Millennia Cards', 'Millennia Credit Cards are launched for the premium customers of EazyBank. With these cards, you will get 5% cashback for each purchase',
CURDATE() - INTERVAL 30 DAY, CURDATE() + INTERVAL 30 DAY, CURDATE(), null);
INSERT INTO `notice_details` ( `notice_summary`, `notice_details`, `notic_beg_dt`, `notic_end_dt`, `create_dt`, `update_dt`)
VALUES ('COVID-19 Insurance', 'EazyBank launched an insurance policy which will cover COVID-19 expenses. Please reach out to the branch for more details',
CURDATE() - INTERVAL 30 DAY, CURDATE() + INTERVAL 30 DAY, CURDATE(), null);
CREATE TABLE `contact_messages` (
`contact_id` varchar(50) NOT NULL,
`contact_name` varchar(50) NOT NULL,
`contact_email` varchar(100) NOT NULL,
`subject` varchar(500) NOT NULL,
`message` varchar(2000) NOT NULL,
`create_dt` date DEFAULT NULL,
PRIMARY KEY (`contact_id`)
);
- other scripts to create account number for other users
INSERT INTO `accounts` (`customer_id`, `account_number`, `account_type`, `branch_address`, `create_dt`)
VALUES (2, 123456, 'Savings', '123 Main Street, New York', CURDATE());
- can post to
localhost:8888/register
{
"name": "John Doe",
"email": "1@1.com",
"mobileNumber": "1234567890",
"pwd": "12345",
"role": "user"
}
- there is a sign up link in the UI but it is not configured
- Cross Origin Resource Sharing
- when making requests across origin
- have to configure if we want to talk cross origin
- default is to block this
- sprinsecuritysec7/src/main/java/com/eazybytes/config/ProjectSecurityConfig.java
-
The browser sends a preflight request to the server and this is where origin data is communicated
-
can use the
@CrossOrigin
annotation -
could explicitly configure it to an endpoint or to '*'
-
It may not be feasible to do this for every single controller in an application
-
can configure globally using spring security instead inside the defaultSecurityChain
- configuration is fed into HttpSecurity
- can see configuration: sprinsecuritysec6/src/main/java/com/eazybytes/config/ProjectSecurityConfig.java
-
Cross Site Request Forgery
-
Spring security blocks updates to post/put operations that update data by default
-
could turn it off with
http().csrf().disable()
, not for production -
if this is on and not configured and posts will get a 401, have to explicitly say who is allowed to post/put
- using a secondary secure CSRF token that is injected in a header by the app, to ensure th request is not being forged
- can use to expose certain public endpoints beyond csrf protection
csrf().ignoringRequestMatchers("/contact", "/register")
- use to be
ignoreAntMatchers()
-
by default spring
CookieCsrfTokenRepository
will create a token with the nameXSRF-TOKEN
-
Sinec Spring v3 we need a
OncePerRequestFilter
to create the header on each request -
sprinsecuritysec7/src/main/java/com/eazybytes/filter/CsrfCookieFilter.java
- sprinsecuritysec6/src/main/java/com/eazybytes/config/ProjectSecurityConfig.java
...
//-------------------- CSRF--------------------------
//tell spring security these are public apis, but will protect all other endpoints
//csrf process against malicious posts/puts so don't need notices as it only gets
.csrf().ignoringRequestMatchers("/contact", "/register")
// create a CSRF token and send it as a cookie, and with withHttpOnlyFalse(), it allows the UI to read the cookie via JavaScript
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
//filter responsible for injecting the cookie
.and().addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
...
- have to handle reading and return the cookie in javascript on the frontend
- angular_ui/bank-app-ui/src/app/components/login/login.component.ts
validateUser(loginForm: NgForm) {
this.loginService.validateLoginDetails(this.model).subscribe(
responseData => {
this.model = <any> responseData.body;
this.model.authStatus = 'AUTH';
window.sessionStorage.setItem("userdetails",JSON.stringify(this.model));
// get the cookie and store is in session storage
let xsrf = getCookie('XSRF-TOKEN')!;
window.sessionStorage.setItem("XSRF-TOKEN",xsrf);
this.router.navigate(['dashboard']);
});
}
- retrieve the cookie and set it in session storage
- angular_ui/bank-app-ui/src/app/interceptors/app.request.interceptor.ts
//get the token from session storage and put it in the headers
let xsrf = sessionStorage.getItem('XSRF-TOKEN');
if(xsrf){
httpHeaders = httpHeaders.append('X-XSRF-TOKEN', xsrf);
}
- with this configuration we now go through the
BasicAuthenticationFilter
as we post authentication to the/user
endpoint - the
BasicAuthenticationFilter
calls theProviderManager
which calls theEazyBankUsernamePwdAuthenticationProvider.authenticate()
method, this fetches the user and does the authentication by callingPasswordEncoder.matches()
method, if it does match then theUsernamePasswordAuthenticationToken
constructor si called which creates the auth token and sets authenticated to true - the authentication information is encoded on a
Authorization Basic:
header - this is not recommend for production apps as the username and password are not encrypted on the header, they only exist in as base 64
- authorities are passed to
UsernamePasswordAuthenticationToken
upon creation as an unmodifiable list - authorities and roles are very similar and used to create
GrantedAuthorities
- create an authorities table and use the id as a foreign key for the user
- see ./database_seed.sql
- have to create a new entity
Authority
to account for new table in db - sprinsecuritysec7/src/main/java/com/eazybytes/model/Authority.java
- have to map
Authority
model,@manyToOne
toCustomer
model - this will pull authorities based on primary/foreign key from the db and map them to the customer object upon request of the customer
- sprinsecuritysec7/src/main/java/com/eazybytes/config/ProjectSecurityConfig.java
hasAuthority
- have to have the exact authorityhasAnyAuthority
, if they have any of the acceptable ones- if neither of above options work use
access()
which allows us to configure using spring expression language
-
can invoke after
requestMatchers
-
requestMatchers
replacedantMatchers
in spring v 3 -
see example
http.
...
.authorizeHttpRequests()
// performing RBAC with authorities
.requestMatchers("/myAccount").hasAuthority("VIEWACCOUNT")
.requestMatchers("/myBalance").hasAnyAuthority("VIEWACCOUNT","VIEWBALANCE")
.requestMatchers("/myLoans").hasAuthority("VIEWLOANS")
.requestMatchers("/myCards").hasAuthority("VIEWCARDS")
//secure these paths so only authenticated users can access
.requestMatchers( "/user").authenticated()
//allow anyone to see this
.requestMatchers("/notices", "/contact", "/register").permitAll();
...
return http.build();
- as of spring boot 3 and security 6 we now need to explicitly tell spring to always create new sessions
- this will create a new session each time the user logs in
- if you restart the server, must re-login to get a valid JSessionID
.and().sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
- authority is one individual privilege (fine grained)
- role is a group of privileges/actions (course grained)
- roles also us
GrantedAuthority
andSimpleGrantedAuthority
- when using roles it should awalys start with the
ROLE_
prefix as to differentiate between authorities and roles
- sprinsecuritysec7/src/main/java/com/eazybytes/config/ProjectSecurityConfig.java
hasRole
- have to have the exact authorityhasAnyRole
, if they have any of the acceptable ones- if neither of above options work use
access()
which allows us to configure using spring expression language - DO NOT USE THE ROLE PREFIX IN CODE, spring adds the prefix value
- new sequence flow with roles
- could use
hasIPAddress
to really secure the app (may have bee deprecated in version 3)
- security filters play a vital role inside spring security
-
you could create a custom filter to do whatever you want, such as intercepting and validating requests or adding headers, logging auth data at a certain point, encryption of input data, add MFA, etc...
-
filters are processed in a chain fashion executing sequentially one at a time
-
unless explicitly told, filters can execute in a semi-random order
- changes are not for production, will create large security risk
@EnableWebSecurity(debug = true)
logging.level.org.springframework.security.web.FilterChainProxy=DEBUG
- logging level also turns on
logger.isDebugEnabled
Filter
interface exposesinit()
: empty by default, runs on creation of filterdestroy()
: empty by default runs on destruction of filterdoFilter()
: the main method of a filter, that must be overridden when creating a custom filterdoFilterInternal()
: another method, which is called bydoFilter()
, we have to override this in some cases when we cannot overide the normaldoFilter()
, such as theOncePerRequestFiler
-
sprinsecuritysec8/src/main/java/com/eazybytes/config/ProjectSecurityConfig.java
-
create the new filter: sprinsecuritysec8/src/main/java/com/eazybytes/filter/RequestValidationBeforeFilter.java
-
also have to add code to
defaultSecurityFilterChain
to add in the new filter
//adding custom validation filter before the BasicAuthenticationFilter in the filter chain
//filter we ant to add, where we want it to go
.addFilterBefore(new RequestValidationBeforeFilter(), BasicAuthenticationFilter.class )
-
If we wanted to add a logger after authentication to log who logs in
-
sprinsecuritysec8/src/main/java/com/eazybytes/filter/AuthoritiesLoggingAfterFilter.java
-
after writing filter have to add it to the
defaultSecurityFilterChain
.addFilterAfter(new AuthoritiesLoggingAfterFilter(), BasicAuthenticationFilter.class)
- now the chain looks like:
- and in the logs we get
User 0@1.com is successfully authenticated and has the authorities [VIEWLOANS, VIEWBALANCE, ROLE_USER, ROLE_ADMIN, VIEWCARDS, VIEWACCOUNT]
-
adds a filter to run around the same time as the other one passed in, however spring will execute these filters in a random order
-
not used a lot as can be unpredictable
-
here we will log right before or afert the
BasicAuthenticationFilter
runs -
sprinsecuritysec8/src/main/java/com/eazybytes/filter/AuthoritiesLoggingAtFilter.java
-
after writing filter have to add it to the
defaultSecurityFilterChain
// adding custom logging filter at the time of the BasicAuthenticationFilter
.addFilterAt(new LoggingAtAuthenticationFilter(), BasicAuthenticationFilter.class)
- after adding this filter the chain now looks like:
- can see all the custom filters we have added so far
- other options for custom filters
- abstract class in spring web
- simple base implementation of
Filter
- superclass for any type of filter
- can provide access to all the config and init parameters
getEnvironment()
,getFilterConfig()
,getServletContext()
,init()
- some of the methods exposed by this class, making all these details available to the subclass
- regular filters are not limited to running once per request, in theory could run many times per request
- A custom filter that extends
OncePerRequestFilter
is guaranteed to run only 1 time per request - extends
GenericFilterBean
- business logic should be inside the
doFilterInternal()
method - other useful methods -
shouldNotFilter()
, can decide to not filter certain requests - example: sprinsecuritysec8/src/main/java/com/eazybytes/filter/CsrfCookieFilter.java
BasicAuthenticationFilter
extendsOncePerRequestFilter
- recommended for use over regular filter due to guarantees that it only runs 1 time
- all methods in this image have been replaced by
requestMatchers()
in spring v3 / spring security v6 - examples of request matchers
JSESSIONID
: tells spring whether user is valid- this is a simple token, good for what it does but wont help with communication between services
- does not hold any user data
- saved as a cookie in the browser, which is tied to the session
- invalidated if the server is restarted
- could be a plain string or more complex structure like a JWT
- usually generated on Login
- token is sent on every request to the backend server through a header, which the server then has to validate
- backend server usually smart enough to understand it has to validate the token
- especially useful to purge tokens if there is a security breach
- helps execute single sign on, and stateless authentication by passing the validated token to different services
- uses base64 encoding
- header - stores metadata in the token, ie: algorithm, type etc
- body - can store any data about the user/issuer here
- signature - can be used to validate the token
- should be validated on each request as to avoid tampering
- to easily decode jwt's use jwt.io
- no database needed as token is stored in browser and comparison is recomputed in the app
- if all communication is within your LAN/Firewall validation still recomended but not required as someone would have to breach the network to modify the JWT
- if you are communicating outside your network you must validate the signature of your JWT to ensure no one has tampered with it
- backend will regenerate the signature hash using it's secret and compare it to the received JWT to see if the JWT has been tampered with in transit
- sprinsecuritysec9/src/main/java/com/eazybytes/config/ProjectSecurityConfig.java
- must add the following dependencies
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
- must turn off
JSESSIONID
generation by settingsessionCreationPolicy
toSTATELESS
.and().sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- then have to configure backend to expose an
Authorization
header to the frontend - in the cors configuration have to add
corsConfiguration.setExposedHeaders(Arrays.asList("Authorization"));
- sprinsecuritysec9/src/main/java/com/eazybytes/config/ProjectSecurityConfig.java
- write a filter to generate the JWT
- only want the JWT generated on initial login
sprinsecuritysec9/src/main/java/com/eazybytes/filter/JWTTokenGeneratorFilter.java
- filter will be added after the
BasicAuthenticationFilter
, so we can verify the user us valid and then create the token
.addFilterAfter(new JWTTokenGeneratorFilter(), BasicAuthenticationFilter.class)
- sprinsecuritysec9/src/main/java/com/eazybytes/filter/JWTTokenValidatorFilter.java
- write filter to validate token
- we want this to run on all authenticated requests except initial login
- will be executed before the
BasicAuthenticationFilter
, to validate the incoming JWT before getting to the authentication filter
.addFilterBefore(new JWTTokenValidatorFilter(), BasicAuthenticationFilter.class)
- it is the clients responsibility on each request to save the token in the browser on login
- angular_ui/ui-section-9/src/app/components/login/login.component.ts
window.sessionStorage.setItem("Authorization", responseData.headers.get("Authorization")!);
- also the clients responsibility to send token back to backend on request -angular_ui/ui-section-9/src/app/interceptors/app.request.interceptor.ts
let authorization = sessionStorage.getItem("Authorization")
if(authorization){
httpHeaders = httpHeaders.append("Authorization", authorization)
}
-
with changes from this section should now see
- JWT is stored in session storage with the key
Authorization
- Only the
XSRF-TOKEN
is created and stored in cookies, there should be no moreJSESSIONID
as we turned that off
if you still have a
JSESSIONID
there is a good chance it is cached - JWT is stored in session storage with the key
-
now the filter chain holds our new JWT filters
- If the JWT is expired the backend will throw an
ExpiredJWT
exception and the user should log back in - the logic to check for this should be in our
JWTTokenValidationFilter
- compliments RBAC, does not replace
- method security must be enabled with
@EnableMethodSecurity
, as it is disabled by default - allows for fine grained access control
- accepts spring expression language as well as any role and authority methods
-
post authorization is used to validate the data being returned from the method
-
PermissionElevator.hasPermission()
allows much more fine grained authorization -
uses spring AOP at runtime to intercept method calls and ensure users have the correct permissions
- allows for use of spring expression language
- most common to be used
- example sprinsecuritysec10/src/main/java/com/eazybytes/repository/LoanRepository.java
@PreAuthorize("hasRole('User')")
- allows for use of spring expression language
- sprinsecuritysec10/src/main/java/com/eazybytes/controller/LoansController.java
- runs after the method, spring will prevent the anything from being return if the user does not have the correct permissions
@PostAuthorize("hasRole('USER')")
- this is not related to Spring Security Filter
- can filter the method call based on the parameters being passed in
filterObject
, the method input, should always be of typeCollection
- could say, get this collection but don't return items where the userName = "test"
-
for filtering parameters passed into a method, ie: if they pass in 'test' then throw an error
-
sprinsecuritysec10/src/main/java/com/eazybytes/controller/ContactController.java
@PreFilter("filterObject.contactName != 'Test'")
- for filtering returned values from a method, ie: don't return any elements where the name is 'test'
- sprinsecuritysec10/src/main/java/com/eazybytes/controller/ContactController.java
@PostFilter("filterObject.contactName != 'Test'")
- All theory in this section, no corresponding code
- if you use microservices/multiple applications it can be better to have an authentication server instead of configuring/maintaining authentication/authorization logic in every application
- by sharing a limited authorization token we can give the 3rd party access to the app without having to trust that application isn't going to mis-use their permissions
- could revoke these tokens
- there is a separate OAuth2 server handling the authentication and token creation
- OAuth2 is a standard, there are many different flavors.
- there is no OAuth2 code
- different grant types are for different use cases
- Authorization Code - user accounts
- PKCE - similar to Authorization code but has a different way for teh client (A Browser based SPA in most cases) to login to the
Auth Server
, this is becuase in a browser based application it is very difficult to protect theClient Secret
. - Client Credentials - server to server communication, when there is no user involved
- Device Code - for IOT such as smart tv
- Refresh Token - used to refresh the
Access Token
- Implicit Flow (deprecated, will be remove with Oauth2.1)
- Password Grant (deprecated, will be remove with Oauth2.1)
- its time for OAuth2.1
- removing unused grants
- simplifying overall standards
- Resource Owner : the user who owns the resources being accessed
- Client : the third party app/service trying to interact with the
Resource Server
on behalf of theClient
- Authorization Server : responsible for authenticating the
Resource Owner
Resource Owner
has an account on this server- also responsible for generating access tokens and redirecting back the the
Client
after authentication is complete
- Resource Server : server where the resources the
Client
wants to consume are located- depending on the size of the project the
Authorization Server
andResource Server
could be the same machine
- depending on the size of the project the
- Scopes : granular permissions, very similar to roles/authorities
- fictional scenario for example
- Must register app as a
Client
with theAuthorization Server
- when you register as a
Client
you get aClient ID
andClient Secret
- on login
Client
must redirect to theAuthorization Server
, user credentials are never shared with theClient
- Then
Resource Owner
asked if they consent to sharing resources withClient
- If the
Resource Owner
agrees then anAccess Token
andRefresh Token
are shared with theClient
from theAuthorization Server
Authorization Server
decides how long the tokens are good for
Client
sends an api requests withAccess Token
to theResource Server
. TheResource Server
verifies the token is valid- the
Access Token
is scoped to only the permissions needed by theClient
. - the
Client
cannot update account credentials etc...
- the
Resource Server
sends requested resources toClient
- most times you will be redirected back to the
Client
after providing consent
-
sign up for any account with google, facebook, github etc...
-
example using slack
Resource Owner
: youClient
: slackAuthorization Server
: run by googleResource Server
: run by googleAccess Token
: provided byAuthorization Server
-
most real customers may have their own
Authorization Server
- have to choose grant type based on use case
- use when we have a
Resource Owner
, aClient
and aResource Server
, where theClient
wants to communicate with theResource Server
behalf of theResource Owner
- more secure and has superseded
Implicit Grant
,
ClientID
andClientSecret
shared from client toAuthorization Server
Resource Owner
requests loginClient
redirectsResource Owner
to theAuth Server
, also appends important information such asClientID
to tellAuth Server
whatClient
to give access to,Scope
for type of access, andRedirect Uri
to get back to originalClient
Resource Owner
logs intoAuth Server
Auth Server
providesClient
withAuthorization Code
Client
providesAuth Server
withAuthorization Code
,ClientID
,ClientSecret
,Grant Type
andRedirect URI
- if all checks out
Auth Server
providesClient
withAccess Token
, in the form of aJWT
Client
requests resources on fromResource Server
on behalf of theResource Owner
and sends theAccess Token
to authenticate- Resources returned to the
Client
from theResource Server
- Deprecated, not to be used for production
- will be removed from OAuth2.1
- less secure because there is no way to send the
ClientSecret
in get requests outside of the Url. This would exposing the client credentials it to the world - The url also contained the
Access Token
, exposing it via query params - No way to reliably prevent a malicious user from stealing the
Acces Token
-
also know as
Resource Owner Credentials Grant
-
no recommended for production
-
in this case
Resource Owner
credentials are given directly to theClient
-
the
Client
then posts theResource Owner
credentials,ClientID
andClientSecret
to theAuthorization Server
-
Authorization Server
sends back anAccess Token
to theClient
which theClient
can then use to access theResource Server
on behalf of theResource Owner
-
could be used if all the parties are part of the same organization, LAN
-
being removed in Oauth2.0+
- this is for server to server communication only
- only have a
Client
,Auth Server
andResource Server
, no user - grant_type will be 'client_credentials'
Client
sends request toAuth Server
, it states there is no end user and providesClientID
andClient Secret
Auth Server
, providesAccess Token
toClient
Client
sends request toResource Server
with theAccess Token
- If all is good, the
Resource Server
sends back resources to theClient
- inside the response after a successfully authenticating, there is usually a
Refresh Token
- The
Refresh Token
will used to refresh theAccess Token
after x amount of time Client
uses theRefresh Token
to request a newAccess Token
from theAuth Server
, if theRefresh Token
is valid then a newAccess Token
is provided- does not require end user to login again
- it is the job of the
Client
application to implement this refresh token configuration
Resource Server
must reach out toAuthorization Server
with the providedAccess Token
to validate that token onClient
requests
- store all valid tokens in a common database and check if the
Access Token
is present
Resource Server
has the public key ofAccess Server
, it can use the public key to validate anyAccess Tokens
- most common and recommended approach
- not a grant type flow
- OIDC is a wrapper around OAuth2
- OIDC (Open ID Connect) sits on top of OAuth2 for authentication, and brings standards for sharing identity details
- there is no way with OAuth2 to know details about the client, ie: username, email etc...
- The
Authorization Server
is smart enough to know that ifopenid
exists in theScope
, then it returns both anAccess Token
andID Token
- With
Access Token
andID Token
we now have IAM, a more advanced concept implemented by Keycloak and other OAuth2 providers
- using a separate app for this section only - ./12_section/springsecOAUTH2GitHub
-
will use Github as OAuth2
Auth Server
-
the spring boot app will be the
Client
in this scenario -
sign into Github -> settings -> developer settings -> OAuth Apps -> Register New Application
- Github supplies us with a
ClientID
and we have to generate theClientSecret
- could add logo if you want
-
12_section/springsecOAUTH2GitHub/src/main/java/com/eazybytes/springsecOAUTH2GitHub/config/SpringSecOAUTH2GitHubConfig.java
-
must include the oauth2 client dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
- passing
ClientID
andClientSecret
through env vars in the application properties
- have to add to either bash profile
export github_client_id=myid
export github_client_secret=mysecret
- or to the evnironment variables in the intellij configurations
- edit configurations -> modify options -> environment varaibles
- must be seperarted by ;
- If configured correcttly you should be redirected to the github login page and see you are logging in at teh request of the
Client
, you provide you credentails and then verify you want to give theClient
access to your informtaion. Github will then redirect you back to theClient
and provide and access key. if configured correctly you should be redirected to github login page, be able to login, grant your app access to your github account and be redirected - good for small apps but, limits your control and does not allow for creation of roles/scopes/users etc...
- will be demoing client credentials, authorization code and pkce grant types
- https://www.keycloak.org/
- an authorization server
- other commmon ones are okta and amazon congnito
- open source
- very stable and continually updated
- maintained by RedHat
- download the ZIP and extract it
- openkJDK getting started docs
- cd to dircttion and run
bin/kc.sh start-dev --http-port 8180
, runs keycloak in dev mode at port 8180 - create and admin user then go to the admin console
- this version will use an h2 in memory db
- if you use the keycloak in this repo, usernam
admin
, passswordadmin
- Realm: space in auth server, should have different realms for dev, staging, prod etc..
- create a new realm called
eazybankdev
- can create a client through the UI, in a real world situation, would have to reach out to the maintainer of the keycloak server and go through some sort of review before being added as a
Client
- clients tab -> new client ->
- type: OpenID Connect
- clientID: eazybankapi
- turn on client authentication
- right now we are only testing server to server communication, there is no
Resource Owner
involved, so uncheck Standard Flow and Direct Access Grants, and check Service Account Roles (this will enable client credentials grant type) - cliendID:
eazybankapi
, clientSecret: can be found under the credentials tab of the client
-
if you used the same realm name can use this endpoing to see importnat uri's
-
http://localhost:8180/realms/eazybankdev/.well-known/openid-configuration
-
have to convert our backend system into a resource server
- have to add the maven dependency for OAuth2 resource server
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency>
- then have to configure http to use
- sprinsecuritysec13/src/main/java/com/eazybytes/config/ProjectSecurityConfig.java
// telling spring this will now act as an OAuth resource server // it will receive access tokens in the form of JWT's and use the jwtAuthenticationConverter class to get the granted authorities .and().oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter);
-
then have to add to application.proprties, where the url is your realm uri
-
Have to implement a class/method to convert the roles (which are strings) in the JWT to GrantedAuthorities so spring knows how to handle them
- sprinsecuritysec13/src/main/java/com/eazybytes/config/KeycloakRoleConverter.java
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8180/realms/eazybankdev/protocol/openid-connect/certs
- have to get the
Access Token
from theAuthentication Server
, under the client
-
sent a post request with required headers to Keycloak, this returned us back an
Access Token
, in the form of a JWT, upon successfulClient
Authentication -
must incluce the scope of
openid
at the minimum -
this is an example post our server may make to an
Authentication Server
-
under the Realm Roles Tab, create a role with the name
USER
andADMIN
-
have to assign these roles to the client now.
- go to clients and eazybankapi
- DONT DO THIS UNDER THE ROLES TAB, THE ROLES TAB IS USED FOR END USERS
- eazybankapi is a service account so click the 'service accounts roles' tab and add the roles required
- now will have the roles in your JWT
- can decode at https://jwt.io
- can take the JWT from the previous step and pass it to the
Resource Server
, eazbank backend in this case and we should be authorized by this line in the security configuraiton
.and().oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter);
- here the
jwtAuthenticationConverter
, which we set as our won custom implementation ofKeycloakRoleConverter
converts the strings in the 'realm-access' key to granted authorities which can then be read by spring to authorize the user
- not for javascript single page applications
-
have to create a new client in keycloak,
- 'easzybankclient'
- 'client authenticaiton' on
- 'standard flow' checked
- uncheck 'Direct Access Grants'
-
after creating the client you must also add the valid redirect uri under client settings.
-
dont have one for this section
-
have to create a user under the
Users
tab in the keycloak console
Resource Owner
is redirected by theClient
to the keycloak authroization endpoint- in this case that is
http://localhost:8180/realms/eazybankdev/protocol/openid-connect/auth
- in this case only test with postman, as frontend app is configured for pkce
- There is a potman collection json file in the root, import this into postman, can use this to mock this work flow, inside the authorization code folder, read the directions descriptions
also includes postman request for client credentials, jwt and creating basic users
- we have an angular app and a spring boot app, we need to implement the Authorization Code flavor PKCE.
- this is becuase with regular Authorization Code there is no good way to hide the client secret value in the code by looking at the source code in the browser
-
removes the need for use of client secret, for SPA's that are browser based and public facing we should now use the
code_verifier
andcode_challenge
to authenticate theClient
-
designed to protect the authorization work flow
-
recomended approach
-
going to be combined with Authorization Code in Oauth2.1
-
Resource Owner
requsts access to resource oncClient
-
Client
tellsResource Owner
to talk toAuth Server
-
Resource Owner
is redirected toAuth Server
, where they login in with their credentials, and pass along theclient_id
redirect_uri
scope
- always have to cinlude openidstate
- protect against csrfresponse_type
- shoudl be codecode_challenge_method
- what algrorithm was usedcode_challenge
, which was generated by theClient
-
Auth Server
providesClient
with and one timeAuthz Code
-
Client
responds to theAuth Server
and provides theclient_id
,Authz Code
, andcode_verifier
which theAuth Server
can then use to calculate thecode_challenge
and validate the request -
Auth Server
provides theClient
with anAccess Token
-
Client
requests resources from theResource Server
and sends along theAccess Token
-
Resource Server
providesClient
the resources
- create a new
Client
client authentication
should be off becuase we do not want to be fore to share the client secret with theAuth Server
- only keep
Standard Flow
checked - must add the valid redirect uri to the client settings,
http://localhost:4200/dashboard
- can also add a logout route if you want,
http://localhost:4200/home
in this case - add a web origin of
http://localhost:4200
or you will be blocked by cors - under the client advanced menu set
Proof Key for Code Exchange Code Challenge Method
fors256
- have to implement this inside the angular application
- have to install keycloak angular npm package, already included in the package.json
- inside the angular.json, have to allow som dependencies
"allowedCommonJsDependencies": [
"base64-js",
"js-sha256"
],
- alot of change dont ie ui in terms of path arguments, configuration, can find the working application in ./angular-ui/ui-section-13
- in short needed to complete steps above, set up keycloak auth guard in the app module and protect the routes in the app routing module, via the new auth.routeguard
- also had to create login and logout methods and wire them to the buttons in the UI
- In this PKCE configuration we have configured our frontend to authenticate with an
Auth Server
, get the response and when sedning an api request to the backend we attatch the JWT as anAuthorization
header. Then in the backend we get the JWT and strip the roles off, turn them intoGranted Authorities
, and store them in the security context of the logged in user
- Comes with alot of features out of the box
- Can provide your own custom login page under realm settings -> themes
- Provides an admin rest api for completing all admin tasks
- Can provide deafult client scopes, so they are alawys returned to the user, could also set to optional or none
- Can create own client scopes
- Offers groups/roles
- Can see/manage all active sessions
- Can log events
- Can define password policies under the Authentication tab
- Can leverage identity providers for social login
- Supports integration with Kerberos and Ldap