I am currently building an android/ios/web app in flutter that uses oauth2 authentication with backend spring boot oauth2 server.Since I am new on flutter I don't have any idea about implementing the above on flutter.I want to implement a login screen which uses 'password' grant type to request access and need to implement access token refresh mechanism using refresh token before the token gets expired.I need to implement a multiuser login with token management like in gmail app where authenticated accounts/users can switched at any time with convenience. The following link provide way to authenticate using client credential and authorization grant using oauth2_client library. client credential and authorization grant
Please help me to implement this.
Below is my backend AuthorizationServerConfigurerAdapter configuration.
import com.exp.myserver.model.Account;
import com.exp.myserver.model.CustomUserDetails;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@Configuration
@EnableAuthorizationServer
public class MyOAuthConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
private static final String RESOURCE_ID = "restservice";
static Logger logger = LoggerFactory.getLogger(OAuth2AuthorizationConfig.class);
private static final int VALID_FOREVER = -1;
// TODO externalize token related data to configuration, store clients in DB
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
logger.info(" configure(ClientDetailsServiceConfigurer clients) ");
clients
.inMemory()
.withClient("clientId")
.authorizedGrantTypes("password","refresh_token")
.authorities("ROLE_ADMIN","ROLE_CLIENT","ROLE_SUPPORT","ROLE_MANAGER","ROLE_ACCOUNT")
.scopes("read", "write")
.resourceIds(RESOURCE_ID)
.secret(passwordEncoder.encode("clientPassword")).accessTokenValiditySeconds(4800).refreshTokenValiditySeconds(VALID_FOREVER);//Integer.MAX_VALUE
}
/*
* The endpoints can only be accessed by a not logged in user or a user with
* the specified role
*/
// TODO externalise configuration
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
logger.info(" configure(AuthorizationServerSecurityConfigurer oauthServer) ");
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_ADMIN')")
.checkTokenAccess("hasAuthority('ROLE_ADMIN')");
oauthServer.allowFormAuthenticationForClients();
oauthServer.checkTokenAccess("permitAll()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
logger.info(" configure(AuthorizationServerEndpointsConfigurer endpoints) ");
endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtAccessTokenConverter())
.authenticationManager(authenticationManager);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
// TODO encrypt password
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
logger.info(" jwtAccessTokenConverter() ");
JwtAccessTokenConverter converter = new CustomTokenEnhancer();
converter.setKeyPair(new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "keystorePassword".toCharArray()).getKeyPair("mykeystore"));
return converter;
}
/*
* Add custom user principal information to the JWT token
*/
// TODO additional information fields should be get from configuration
protected static class CustomTokenEnhancer extends JwtAccessTokenConverter {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
logger.info(" OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) ");
Account user = (Account) authentication.getPrincipal();
Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
CustomUserDetails custUser = (CustomUserDetails) authentication.getPrincipal();
info.put("user_id", user.getId());
info.put("email", user.getUserName());
info.put("user_mode", user.getUserMode());
info.put("company_id", user.getUserCompany().getCompanyId().getId());
info.put("company_name", user.getUserCompany().getCompanyId().getName());
DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
// Get the authorities from the user
Set<GrantedAuthority> authoritiesSet = new HashSet<>(authentication.getAuthorities());
logger.info("GrantedAuthority authorities "+authentication.getAuthorities());
// Generate String array
String[] authorities = new String[authoritiesSet.size()];
logger.info("authoritiesSet.size() "+authoritiesSet.size());
int i = 0;
for (GrantedAuthority authority : authoritiesSet)
authorities[i++] = authority.getAuthority();
info.put("authorities", authorities);
customAccessToken.setAdditionalInformation(info);
return super.enhance(customAccessToken, authentication);
}
}
/*
* Setup the refresh_token functionality to work with the custom
* UserDetailsService
*/
@Configuration
protected static class GlobalAuthenticationManagerConfiguration extends GlobalAuthenticationConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
logger.info(" Oauth init(AuthenticationManagerBuilder auth) ");
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
logger.info(" Oauth init(AuthenticationManagerBuilder auth) "+auth.getDefaultUserDetailsService().toString());
}
}
}
I used a CustomTokenEnhancer to provide extra information for the authenticated user upon login. I am able to login using oauth2 library in flutter as below but I can not access the user information from the oauth client.
import 'package:oauth2/oauth2.dart' as oauth2;
Future<oauth2.Client> authenticateUser() async {
// This URL is an endpoint that's provided by the authorization server. It's
// usually included in the server's documentation of its OAuth2 API.
final authorizationEndpoint = Uri.parse('${selCompanyUrl}oauth/token');
// The user should supply their own username and password.
final uName = username;
final uPassword = password;
// The authorization server may issue each client a separate client
// identifier and secret, which allows the server to tell which client
// is accessing it. Some servers may also have an anonymous
// identifier/secret pair that any client may use.
//
// Some servers don't require the client to authenticate itself, in which case
// these should be omitted.
final identifier = 'clientID';
final secret = 'ClientPassword';
// Make a request to the authorization endpoint that will produce the fully
// authenticated Client.
var client = await oauth2.resourceOwnerPasswordGrant(
authorizationEndpoint, uName!, uPassword!,
identifier: identifier, secret: secret);
print('Oauth Client : $client :: access token : ${client.credentials.accessToken} :: refresh token : ${client.credentials.refreshToken}');
return client;
}