Skip to content

Commit

Permalink
Feature/#1206 (#1207)
Browse files Browse the repository at this point in the history
* Added UserInfoOidcAuthenticator to authenticate a user based on access token

* Added integration tests for UserinfoOidcAuthenticator

* Fixed a bug

* Added java docs in new files

* Added documentation and managed dependencies
  • Loading branch information
rockydcoder authored and leleuj committed Nov 27, 2018
1 parent 6a86b94 commit e3ece6e
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 3 deletions.
21 changes: 21 additions & 0 deletions documentation/docs/clients/openid-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ You need to use the following module: `pac4j-oidc`.

## 2) Clients

#### Indirect clients

For any OpenID Connect identity provider, you should use the generic [OidcClient](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/client/OidcClient.java) (or one of its subclasses) and the [`OidcConfiguration`](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/config/OidcConfiguration.java) to define the appropriate configuration.

Note: *[OidcClient](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/client/OidcClient.java) can be used only for indirect clients (web browser based authentication)*

Before *pac4j* v1.9.2, the configuration was directly set at the client level.

**Example**:
Expand Down Expand Up @@ -77,6 +81,23 @@ You can request to use the `nonce` parameter to reinforce security via:
config.setUseNonce(true);
```

#### Direct clients

For direct clients (web services), you can get the `access token` from any OpenID Connect identity provider and use that in your request to get the user profile.

For that, the [HeaderClient](https://github.com/pac4j/pac4j/blob/master/pac4j-http/src/main/java/org/pac4j/http/client/direct/HeaderClient.java) would be appropriate, along with the [UserInfoOidcAuthenticator](https://github.com/pac4j/pac4j/blob/master/pac4j-oidc/src/main/java/org/pac4j/oidc/credentials/authenticator/UserInfoOidcAuthenticator.java).

```java
OidcConfiguration config = new OidcConfiguration();
config.setClientId(clientId);
config.setSecret(secret);
config.setDiscoveryURI(discoveryUri);
UserInfoOidcAuthenticator authenticator = new UserInfoOidcAuthenticator(config);
HeaderClient client = new HeaderClient("Authorization", "Bearer ", authenticator);
```

The request to the server should have an `Authorization` header with the value as `Bearer {access token}`.

## 3) Advanced configuration

You can define how the client credentials (`clientId` and `secret`) are passed to the token endpoint with the `setClientAuthenticationMethod` method:
Expand Down
3 changes: 3 additions & 0 deletions documentation/docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ title: Release notes:

**v3.5.0**:

- Added `UserInfoOidcAuthenticator` to authenticate a user based on access token received from an OIDC authenticator


**v3.4.0**:

- Added ability to create a composition of authorizers (conjunction or disjunction)
Expand Down
15 changes: 15 additions & 0 deletions pac4j-oidc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
<artifactId>pac4j-jwt</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-http</artifactId>
<scope>test</scope>
<type>test-jar</type>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand All @@ -62,6 +68,15 @@
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.nanohttpd</groupId>
<artifactId>nanohttpd</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
<!-- for testing -->
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.pac4j.oidc.credentials.authenticator;

import static java.util.Optional.ofNullable;

import java.io.IOException;

import javax.naming.AuthenticationException;

import org.pac4j.core.context.WebContext;
import org.pac4j.core.credentials.TokenCredentials;
import org.pac4j.core.credentials.authenticator.Authenticator;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.core.util.CommonHelper;
import org.pac4j.oidc.config.OidcConfiguration;
import org.pac4j.oidc.profile.OidcProfile;
import org.pac4j.oidc.profile.OidcProfileDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;

/**
* The OpenId Connect authenticator by user info.
*
* @author Rakesh Sarangi
* @since 3.5.0
*/
public class UserInfoOidcAuthenticator implements Authenticator<TokenCredentials> {

private static final Logger logger = LoggerFactory.getLogger(UserInfoOidcAuthenticator.class);

private OidcConfiguration configuration;

public UserInfoOidcAuthenticator(OidcConfiguration configuration) {
CommonHelper.assertNotNull("configuration", configuration);
this.configuration = configuration;
}

@Override
public void validate(TokenCredentials credentials, WebContext context) {
final OidcProfileDefinition profileDefinition = new OidcProfileDefinition();
final OidcProfile profile = (OidcProfile) profileDefinition.newProfile();
final BearerAccessToken accessToken = new BearerAccessToken(credentials.getToken());
profile.setAccessToken(accessToken);
final JWTClaimsSet userInfoClaimsSet = fetchOidcProfile(accessToken);
ofNullable(userInfoClaimsSet)
.map(JWTClaimsSet::getClaims)
.ifPresent(claims -> profileDefinition.convertAndAdd(profile, claims, null));

// session expiration with token behavior
profile.setTokenExpirationAdvance(configuration.getTokenExpirationAdvance());

credentials.setUserProfile(profile);
}

private JWTClaimsSet fetchOidcProfile(BearerAccessToken accessToken) {
final UserInfoRequest userInfoRequest = new UserInfoRequest(configuration.findProviderMetadata().getUserInfoEndpointURI(),
accessToken);
final HTTPRequest userInfoHttpRequest = userInfoRequest.toHTTPRequest();
try {
final HTTPResponse httpResponse = userInfoHttpRequest.send();
logger.debug("Token response: status={}, content={}", httpResponse.getStatusCode(),
httpResponse.getContent());
final UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);
if (userInfoResponse instanceof UserInfoErrorResponse) {
logger.error("Bad User Info response, error={}",
((UserInfoErrorResponse) userInfoResponse).getErrorObject());
throw new AuthenticationException();
} else {
final UserInfoSuccessResponse userInfoSuccessResponse = (UserInfoSuccessResponse) userInfoResponse;
final JWTClaimsSet userInfoClaimsSet;
if (userInfoSuccessResponse.getUserInfo() != null) {
userInfoClaimsSet = userInfoSuccessResponse.getUserInfo().toJWTClaimsSet();
} else {
userInfoClaimsSet = userInfoSuccessResponse.getUserInfoJWT().getJWTClaimsSet();
}
return userInfoClaimsSet;
}
} catch (IOException | ParseException | java.text.ParseException | AuthenticationException e) {
throw new TechnicalException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.pac4j.oidc.credentials.authenticator;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.net.URI;
import java.net.URISyntaxException;

import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Answers;
import org.pac4j.core.context.MockWebContext;
import org.pac4j.core.credentials.TokenCredentials;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.core.util.TestsConstants;
import org.pac4j.http.test.tools.ServerResponse;
import org.pac4j.http.test.tools.WebServer;
import org.pac4j.oidc.config.OidcConfiguration;
import org.pac4j.oidc.profile.OidcProfile;

import fi.iki.elonen.NanoHTTPD;

/**
* Tests {@link UserInfoOidcAuthenticator}.
*
* @author Rakesh Sarangi
* @since 3.5.0
*/
public class UserInfoOidcAuthenticatorIT implements TestsConstants {

private static final int PORT = 8088;

@BeforeClass
public static void setUp() {
final WebServer webServer = new WebServer(PORT)
.defineResponse("ok", new ServerResponse(NanoHTTPD.Response.Status.OK, "application/json",
String.format("{%n" +
" \"sub\": \"%s\",%n" +
" \"name\": \"%s\",%n" +
" \"preferred_username\": \"%s\"%n" +
"}", ID, GOOD_USERNAME, USERNAME)))
.defineResponse("notfound", new ServerResponse(NanoHTTPD.Response.Status.NOT_FOUND, "plain/text", "Not found"));
webServer.start();
}

@Test
public void testOkay() throws URISyntaxException {
final OidcConfiguration configuration = mock(OidcConfiguration.class, Answers.RETURNS_DEEP_STUBS);
when(configuration.findProviderMetadata().getUserInfoEndpointURI()).thenReturn(new URI("http://localhost:" + PORT + "?r=ok"));
final UserInfoOidcAuthenticator authenticator = new UserInfoOidcAuthenticator(configuration);
final TokenCredentials credentials = getCredentials();

authenticator.validate(credentials, MockWebContext.create());

final OidcProfile profile = (OidcProfile) credentials.getUserProfile();
assertEquals(GOOD_USERNAME, profile.getDisplayName());
assertEquals(USERNAME, profile.getUsername());
assertEquals(credentials.getToken(), profile.getAccessToken().getValue());
}

@Test(expected = TechnicalException.class)
public void testNotFound() throws URISyntaxException {
final OidcConfiguration configuration = mock(OidcConfiguration.class, Answers.RETURNS_DEEP_STUBS);
when(configuration.findProviderMetadata().getUserInfoEndpointURI()).thenReturn(new URI("http://localhost:" + PORT + "?r=notfound"));
final UserInfoOidcAuthenticator authenticator = new UserInfoOidcAuthenticator(configuration);
final TokenCredentials credentials = getCredentials();

authenticator.validate(credentials, MockWebContext.create());
}

private TokenCredentials getCredentials() {
final String token = "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIn0..NTvhJXwZ_sN4zYBK.exyLJWkOclCVcffz58CE-"
+ "3XWWV24aYyGWR5HVrfm4HLQi1xgmwglLlEIiFlOSTOSZ_LeAwl2Z3VFh-5EidocjwGkAPGQA_4_KCLbK8Im7M25ZZvDzCJ1kKN1JrDIIrBWCcuI4Mbw0O"
+ "_YGb8TfIECPkpeG7wEgBG30sb1kH-F_vg9yjYfB4MiJCSFmY7cRqN9-9O23tz3wYv3b-eJh5ACr2CGSVNj2KcMsOMJ6bbALgz6pzQTIWk_"
+ "fhcE9QSfaSY7RuZ8cRTV-UTjYgZk1gbd1LskgchS.ijMQmfPlObJv7oaPG8LCEg";
return new TokenCredentials(token);
}
}
18 changes: 15 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@
<h2.version>1.4.196</h2.version>
<java.version>1.8</java.version>
<jackson.version>2.9.5</jackson.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<nanohttpd.version>2.3.1</nanohttpd.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

<dependencyManagement>
<dependencies>
Expand Down Expand Up @@ -193,6 +194,12 @@
<artifactId>pac4j-sql</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-http</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
</dependency>
<dependency>
<groupId>junit</groupId>
Expand Down Expand Up @@ -234,6 +241,11 @@
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
<dependency>
<groupId>org.nanohttpd</groupId>
<artifactId>nanohttpd</artifactId>
<version>${nanohttpd.version}</version>
</dependency>
<!-- for testing -->
</dependencies>
Expand Down

0 comments on commit e3ece6e

Please sign in to comment.