Good day,
I have a spring-boot 1.1.4.RELEASE app,that is using spring-security included as dependencies such as:
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.security:spring-security-web:4.0.0.M1")
compile("org.springframework.security:spring-security-config:4.0.0.M1")
compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity3:2.1.1.RELEASE')
I have two types of roles: "User" and "Admin". The latter has everything the former, but also access to an administration screen. In my Thymeleaf page, I only display that link to users with an Admin role via, which works just fine:
<li sec:authorize="hasRole('ADMIN')">
<i class="fa fa-link"></i><a th:href="#{/admin}">
Administer User</a>
</li>
However, if I manually type in the url to that page (http://localhost:9001/admin), all roles can access it. I thought i was controlling this via the Security Configuration class:
#Configuration
#EnableWebMvcSecurity
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers( "/" ).permitAll()
.antMatchers("/admin/").hasRole("ADMIN") <== also tried .antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers( "/resources/**" ).permitAll()
.antMatchers( "/css/**" ).permitAll()
.antMatchers( "/libs/**" ).permitAll();
http
.formLogin().failureUrl( "/login?error" )
.defaultSuccessUrl( "/" )
.loginPage( "/login" )
.permitAll()
.and()
.logout().logoutRequestMatcher( new AntPathRequestMatcher( "/logout" ) ).logoutSuccessUrl( "/" )
.permitAll();
http
.sessionManagement()
.maximumSessions( 1 )
.expiredUrl( "/login?expired" )
.maxSessionsPreventsLogin( true )
.and()
.sessionCreationPolicy( SessionCreationPolicy.IF_REQUIRED )
.invalidSessionUrl( "/" );
http
.authorizeRequests().anyRequest().authenticated();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder encoder = new BCryptPasswordEncoder();
auth.userDetailsService( customUserDetailsService ).passwordEncoder( encoder );
}
}
Is there something missing or incorrect in my configuration?
Update:
The solution I used, based upon Dave's answer was to use the following three lines:
.antMatchers( "/admin**" ).hasAuthority("ADMIN" )
.antMatchers( "/admin/" ).hasAuthority( "ADMIN" )
.antMatchers( "/admin/**" ).hasAuthority( "ADMIN" )
This will render a 403 error on the browser. Eventually I will try and get it to redirect to either an error page or "/'.
You only explicitly protected "/admin/" (with a trailing slash). I imagine you need to be more precise than that if you are visiting "/admin" (without a trailing slash).
Related
I am using Rails as a backend API to support a frontend that is made with VueJS. I want to send a POST request from VueJS when a button is pressed on the website, I have used axios to do this
`
methods: {
// Performs a POST request to the rails endpoint and passes the email and pass as parameters
signup() {
if (this.password === this.password_confirmation) {
this.$http.plain
.post("/signup", {
email: this.email,
password: this.password,
password_confirmation: this.password_confirmation
})
// If successful execute signinSuccesful
.then(response => this.signinSuccesful(response))
// If it doesn't run for whatever reason, execute signupFailed
.catch(error => this.signinFailed(error));
}
},`
This should in theory create a POST request that would be received by the API, I have attempted to catch the request like this: post "signup", controller: :signup, action: :create
However, when I look in the console on chrome I get an error when I first load the site:
`0/signin net::ERR_CONNECTION_REFUSED`
And another error when I click the button to send the POST request:
*
Access to XMLHttpRequest at 'http://localhost:3000/signup' from origin
'http://localhost:8080' has been blocked by CORS policy: Request
header field content-type is not allowed by
Access-Control-Allow-Headers in preflight response.
My 'application.rb' file looks like this:
`module RecordstoreBackend
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0
config.api_only = true
config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :patch, :put, :delete, :post, :options]
end
end`
I think the issue is this file, but I am not sure how to configure it. Thanks for your help.
It seems to me like you're missing the host for that request (on your Vue app). Notice the error says 0/signup which indicates that it's sending the request to http://0/signup that is, in turn, "rejecting" the connection.
I'm not familiar with Vue.js structure, but I'd recommend encapsulating the axios call on a plugin that would use the host from an environment configuration file, so that you can have different hosts for localhost and your production env. Or even just add the host to your environment:
methods: {
// Performs a POST request to the rails endpoint and passes the email and pass as parameters
signup() {
if (this.password === this.password_confirmation) {
this.$http.plain
.post(`${process.env.API_HOST}/signup`, {
email: this.email,
password: this.password,
password_confirmation: this.password_confirmation
})
// If successful execute signinSuccesful
.then(response => this.signinSuccesful(response))
// If it doesn't run for whatever reason, execute signupFailed
.catch(error => this.signinFailed(error));
}
},
Reference: https://cli.vuejs.org/guide/mode-and-env.html#environment-variables
If you want to fix it the way you have it now, simply add your host to the axios call:
methods: {
// Performs a POST request to the rails endpoint and passes the email and pass as parameters
signup() {
if (this.password === this.password_confirmation) {
this.$http.plain
.post("http://localhost:3000/signup", {
email: this.email,
password: this.password,
password_confirmation: this.password_confirmation
})
// If successful execute signinSuccesful
.then(response => this.signinSuccesful(response))
// If it doesn't run for whatever reason, execute signupFailed
.catch(error => this.signinFailed(error));
}
},
About the CORS error, check this: https://stackoverflow.com/a/25727411/715444
I'm trying to build a simple login using symofny/security package in Silex, but I have a small problem with authentication checking.
The structure:
/
/login
/admin
/login_check
/logout
In order to get to the /admin route user needs to be authenticated, if he's not, he gets redirected to /login form, which then, as recommended, is asking admin/login_check for authentication (that's all provided by symofny's security firewalls).
The firewalls configuration:
'security.firewalls' => array(
'login' => array(
'pattern' => '^/login$',
),
'admin' => array(
'pattern' => '^/admin',
'http' => true,
'form' => array(
'login_path' => '/login',
'check_path' => '/admin/login'
),
'logout' => array(
'logout_path' => '/admin/logout',
'invalidate_session' => true
),
'users' => ...
),
)
Everything works fine, but the user can enter the /login route even-though he's already authenticated, which is not that bad, but I'd like to avoid it. I tired to check the user authentication status, but I guess it does not work as I'm checking it on /login controller, which is not in "secured" area of the website.
Here's the code:
public function index(Request $request, Application $app)
{
if ($app['security.authorization_checker']->isGranted('ROLE_ADMIN')) {
return $app->redirect($app['url_generator']->generate('admin'));
}
return $app['twig']->render('login/index.twig', array(
'error' => $app['security.last_error']($request),
'last_username' => $app['session']->get('_security.last_username'),
));
}
That throws an error: The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL. So, the question, is there any way to do this using symfony/security natively (without any walk-arounds)? or is it somehow possible to create the /login route inside secured area with possibility to access it even-though the user is not logged in (e.g. make an exception for GET request) which would solve the problem?
UPDATE
I've also added the firewall configuration for /login page with an option anonymous: true, which resulted in not throwing an error anymore, but yet, when I'm logged in on the /admin route, method isGranted('ROLE_ADMIN') results in true, whereas on /login route it results in false (I'm still logged in there).
You can easily understand the behavior of the security component by dumping the current token.
public function index(Request $request, Application $app)
{
var_dump($application['security.token_storage']->getToken());
}
When you don't set anonymous option for login page (by default it is false):
null
When you set anonymous option for login page to true:
object(Symfony\Component\Security\Core\Authentication\Token\AnonymousToken)
private 'secret' => string 'login' (length=5)
private 'user' (Symfony\Component\Security\Core\Authentication\Token\AbstractToken) => string 'anon.' (length=5)
private 'roles' (Symfony\Component\Security\Core\Authentication\Token\AbstractToken) =>
array (size=0)
empty
private 'authenticated' (Symfony\Component\Security\Core\Authentication\Token\AbstractToken) => boolean true
private 'attributes' (Symfony\Component\Security\Core\Authentication\Token\AbstractToken) =>
array (size=0)
empty
Following example describes why you are getting error in your initial code example.
How to share security token?
To share security token between multiple firewalls you have to set the same Firewall Context.
SecurityServiceProvider registers context listener for protected firewall you have declared with pattern security.context_listener.<name>. In your example it is registered as security.context_listener.admin. Thus context you want to use is named admin.
Also a firewall context key is stored in session, so every firewall using it must set its stateless option to false.
What is more, to invoke authentication on login page the anonymous option must be set to true
'security.firewalls' => array(
'login' => array(
'context' => 'admin',
'stateless' => false,
'anonymous' => true,
'pattern' => '^/login$',
),
'admin' => array(
'context' => 'admin',
'stateless' => false,
'pattern' => '^/admin',
'http' => true,
'form' => array(
'login_path' => '/login',
'check_path' => '/admin/login'
),
'logout' => array(
'logout_path' => '/admin/logout',
'invalidate_session' => true
),
'users' => ...
),
)
After the change, if you are logged in on admin panel and login page you will get an instance of Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken as a token and
Symfony\Component\Security\Core\Authentication\Token\AnonymousToken if you are not logged in.
Because of that, you can safely check permission using $app['security.authorization_checker']->isGranted('ROLE_ADMIN') and get the same results on both firewalls.
Currently I am working on rails 4 project, and now I have to link / connect another application (not sso but for accessing API's) say example.com. (Note: example.com uses 3-legged oauth security architecture)
After searching found that I have to implement omniouth strategy.
For this I have refereed this link. As per Strategy-Contribution-Guide I am able to complete setup and request Phase, You can find my sample code here.
require 'multi_json'
require 'omniauth/strategies/oauth2'
require 'uri'
module OmniAuth
module Strategies
class MyAppStrategy < OmniAuth::Strategies::OAuth2
option :name, 'my_app_strategy'
option :client_options, {
site: site_url,
authorize_url: authorize_url,
request_url: request_url,
token_url: token_url,
token_method: :post,
header: { Accept: accept_header }
}
option :headers, { Accept: accept_header }
option :provider_ignores_state, true
def consumer
binding.pry
::OAuth::Consumer.new(options.client_id, options.client_secret, options.client_options)
end
def request_phase # rubocop:disable MethodLength
binding.pry
request_token = consumer.get_request_token({:oauth_callback => callback_url}, options.request_params)
session["oauth"] ||= {}
session["oauth"][name.to_s] = {"callback_confirmed" => request_token.callback_confirmed?, "request_token" => request_token.token, "request_secret" => request_token.secret}
if request_token.callback_confirmed?
redirect request_token.authorize_url(options[:authorize_params])
else
redirect request_token.authorize_url(options[:authorize_params].merge(:oauth_callback => callback_url))
end
rescue ::Timeout::Error => e
fail!(:timeout, e)
rescue ::Net::HTTPFatalError, ::OpenSSL::SSL::SSLError => e
fail!(:service_unavailable, e)
end
def callback_phase # rubocop:disable MethodLength
fail(OmniAuth::NoSessionError, "Session Expired") if session["oauth"].nil?
request_token = ::OAuth::RequestToken.new(consumer, session["oauth"][name.to_s].delete("request_token"), session["oauth"][name.to_s].delete("request_secret"))
opts = {}
if session["oauth"][name.to_s]["callback_confirmed"]
opts[:oauth_verifier] = request["oauth_verifier"]
else
opts[:oauth_callback] = 'http://localhost:3000/auth/callback' #callback_url
end
#access_token = request_token.get_access_token(opts)
super
rescue ::Timeout::Error => e
fail!(:timeout, e)
rescue ::Net::HTTPFatalError, ::OpenSSL::SSL::SSLError => e
fail!(:service_unavailable, e)
rescue ::OAuth::Unauthorized => e
fail!(:invalid_credentials, e)
rescue ::OmniAuth::NoSessionError => e
fail!(:session_expired, e)
end
def custom_build_access_token
binding.pry
verifier = request["oauth_verifier"]
client.auth_code.get_token(verifier, get_token_options(callback_url), deep_symbolize(options.auth_token_params))
end
alias_method :build_access_token, :custom_build_access_token
def raw_info
binding.pry
#raw_info ||= access_token.get('users/me').parsed || {}
end
private
def callback_url
options[:redirect_uri] || (full_host + script_name + callback_path)
end
def get_token_options(redirect_uri)
{ :redirect_uri => redirect_uri }.merge(token_params.to_hash(:symbolize_keys => true))
end
end
end
end
I am able redirect to example.com, also after login I am able to return to my callback_phase (you will ask how did you know, so answer is I have added binding.pry in callback_phase method for checking the flow).
But after executing the strategy I am getting following error
ERROR -- omniauth: (my_app_strategy) Authentication failure! invalid_credentials: OAuth2::Error.
After debugging found that I am getting this error for the super call (from callback_phase method).
First I though may be there are some credentials issue but I am able fetch access token using following (which is executing before the super call)
#access_token = request_token.get_access_token(opts)
Also for more information I am getting error for build_access_token which is the oauth2 method
You can refer this link for more info (just search the build_access_token on the page).
EDIT - 1
After debugging found that getting this issue from the request method.
(While making the faraday request). Here is the code snippet
response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
yield(req) if block_given?
end
Here is my faraday request
#<struct Faraday::Request method=:post, path="example.com/oauth/access_token", params={}, headers={"User-Agent"=>"Faraday v0.9.2", "Content-Type"=>"application/x-www-form-urlencoded"}, body={"grant_type"=>"authorization_code", "code"=>"aPexxxvUg", "client_id"=>"xxxxxur303GXEch7QK9k", "client_secret"=>"xxxxxxcad97b3d252e2bcdd393a", :redirect_uri=>"http://localhost:3000/auth/my_app_strategy/callback"}, options=#<Faraday::RequestOptions (empty)>>
In response I am getting following error message
HTTP Status 400 - Inadequate OAuth consumer credentials.
So can any one help to fix this issue?
Is there any other way to store the access token so that I can utilize this for communication purpose.
Thanks
First of all, I wan to make clear how Oauth2 works:
Oauth2, the protocol says:
You redirect the user to the provider sign in endpoint adding some required parameters (Ejm: PROVIDER/public/oauth?redirect_uri=MYWEB/oauthDemo&
response_type=code&client_id=ABCDE). Sometimes there is also a scope/permission/resource parameter that indicates whats your purpose.
-> Then the users signs in and is redirected to your endpoint MYWEB/public/oauth with a code
Now you have to request the access token doing a POST to the providers endpoint. Example:
POST PROVIDER?code=d5Q3HC7EGNH36SE3N&
client_id=d4HQNPFIXFD255H&
client_secret=1a98b7cb92407cbd8961cd8db778de53&
redirect_uri=https://example.com/oauthDemo&
grant_type=authorization_code
Now you have the access_token and you can use it to get information or decode it using JWT.
Having this clear, and seeing that your call seems corect:
#<struct Faraday::Request method=:post, path="PROVIDER/oauth/access_token", params={}, headers={"User-Agent"=>"Faraday v0.9.2", "Content-Type"=>"application/x-www-form-urlencoded"}, body={"grant_type"=>"authorization_code", "code"=>"aPexxxvUg", "client_id"=>"xxxxxur303GXEch7QK9k", "client_secret"=>"xxxxxxcad97b3d252e2bcdd393a", :redirect_uri=>"MYWEB/auth/my_app_strategy/callback"}, options=#<Faraday::RequestOptions (empty)>>
As the response is "HTTP Status 400 - Inadequate OAuth consumer credentials.", I think maybe you:
a. Your client is not well configured on the Provider. Usually you use to have a basic configuration on the provider site so he can recognise you. So maybe is not well configured.
b. There is a resource/permission/scope parameter missing or wrong configured on the first step (in the redirection to the provider). So when you ask for the token there is a problem.
I am having some difficulty in upgrading to Spring Security 3.2 using Java Config around customizing the RoleVoter to remove the ROLE_ prefix. Specifically, I have this from the original XML:
<!-- Decision Manager and Role Voter -->
<bean id="accessDecisionManager"
class="org.springframework.security.access.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions">
<value>false</value>
</property>
<property name="decisionVoters">
<list>
<ref local="roleVoter" />
</list>
</property>
</bean>
<bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter">
<property name="rolePrefix">
<value />
</property>
</bean>
I have tried to do create the similar configuration in my #Configuration object as such
#Bean
public RoleVoter roleVoter() {
RoleVoter roleVoter = new RoleVoter();
roleVoter.setRolePrefix("");
return roleVoter;
}
#Bean
public AffirmativeBased accessDecisionManager() {
AffirmativeBased affirmativeBased = new AffirmativeBased(Arrays.asList((AccessDecisionVoter)roleVoter()));
affirmativeBased.setAllowIfAllAbstainDecisions(false);
return affirmativeBased;
}
...
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.accessDecisionManager(accessDecisionManager())
.antMatchers("/protected/**").hasRole("my-authenticated-user")
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.logout()
.permitAll();
}
This is where I am now having difficulty, I end up with an exception in the log that look like this:
Caused by: java.lang.IllegalArgumentException: Unsupported configuration attributes: [permitAll, hasRole('ROLE_my-authenticated-user'), permitAll, authenticated, permitAll, permitAll, permitAll]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.afterPropertiesSet(AbstractSecurityInterceptor.java:156) ~[spring-security-core-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.web.configurers.AbstractInterceptUrlConfigurer.createFilterSecurityInterceptor(AbstractInterceptUrlConfigurer.java:187) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.web.configurers.AbstractInterceptUrlConfigurer.configure(AbstractInterceptUrlConfigurer.java:76) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer.configure(ExpressionUrlAuthorizationConfigurer.java:70) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.web.configurers.AbstractInterceptUrlConfigurer.configure(AbstractInterceptUrlConfigurer.java:64) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.configure(AbstractConfiguredSecurityBuilder.java:378) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:327) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:39) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.web.builders.WebSecurity.performBuild(WebSecurity.java:293) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.web.builders.WebSecurity.performBuild(WebSecurity.java:74) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:331) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:39) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration.springSecurityFilterChain(WebSecurityConfiguration.java:92) ~[spring-security-config-3.2.0.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration$$EnhancerByCGLIB$$a7068b50.CGLIB$springSecurityFilterChain$3(<generated>) ~[spring-core-3.2.4.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration$$EnhancerByCGLIB$$a7068b50$$FastClassByCGLIB$$a17f24f9.invoke(<generated>) ~[spring-core-3.2.4.RELEASE.jar:3.2.0.RELEASE]
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228) ~[spring-core-3.2.4.RELEASE.jar:3.2.4.RELEASE]
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:286) ~[spring-context-3.2.4.RELEASE.jar:3.2.4.RELEASE]
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration$$EnhancerByCGLIB$$a7068b50.springSecurityFilterChain(<generated>) ~[spring-core-3.2.4.RELEASE.jar:3.2.0.RELEASE]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.7.0_25]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) ~[na:1.7.0_25]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_25]
at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_25]
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:160) ~[spring-beans-3.2.4.RELEASE.jar:3.2.4.RELEASE]
... 60 common frames omitted
At this point, I am not sure where the ROLE_ is coming from if the RoleVoter is properly configured.
For the _ROLE part you have to use hasAnyAuthority(..) instead of hasAnyRole(..)
From the JavaDoc
If you do not want to have "ROLE_" automatically inserted see
hasAnyAuthority(String)
How to I tell Devise to use https (not http) for all the account confirmation and password reminder etc links?
[note: I'm not looking for a solution to redirect all http to https, I just need devise to ensure the links it creates use https]
Our rails 3 app uses devise, and the app runs fine under https, however, devise always uses http for the email confirmation and password links it emails to users.
In our environment files I tried changing:
config.action_mailer.default_url_options = { :host => "app1.mydomain.com" }
to
{ :host => "https://app1.mydomain.com" }
but predictably devise creates links that look like
http://https//app1.mydomain.com.... (eg, it prepends the :host settings with http:)
default_url_options accepts the same hash parameters as url_for. So you should be able to do this:
config.action_mailer.default_url_options = { :protocol => 'https', :host => 'app1.mydomain.com' }
To set the protocol but also a subdirectory :
config.action_mailer.default_url_options = {
:host => "www.example.com",
:protocol => 'https',
:only_path => false,
:script_name => "/app" #add this attribute if your app is deployed in a subdirectory
}
Source