Skip to content

Commit

Permalink
Merge branch 'release/24.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
cslzchen committed Aug 9, 2024
2 parents 32bb4fa + 38fd246 commit 20a8606
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 9 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.

24.1.0 (08-09-2024)
===================

* Updated 2FA secretparsing to support osf.io python 3.12 upgrade
* Added OAuth guide
* Fixed typo in Institution SSO guide

23.2.1 (07-07-2023)
===================

Expand Down
4 changes: 2 additions & 2 deletions README_INSTITUTION_SSO_INTEGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ For institutions that are not registered with any participant of eduGAIN, COS of
* Required Attribute
* Unique identifier for the user
* User's institutional email
* Either of the follwoing two
* Either of the following two
* User's full name
* User's first **AND** last name
* Attribute name and format
Expand All @@ -68,7 +68,7 @@ For institutions that are not registered with any participant of eduGAIN, COS of

* Provide COS with the metadata URL for your IdP server.

* Provide COS with the entiry ID for your IdP server, which should be the same as the one defined in your metadata.
* Provide COS with the entity ID for your IdP server, which should be the same as the one defined in your metadata.

* It is recommended that a temporary institution test account is created for COS engineers if possible, which will significantly aid and accelerate the process.

Expand Down
286 changes: 286 additions & 0 deletions README_OAUTH_SSO_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
# OSF CAS as an OAuth Server

## About

OSF CAS serves as an OAuth 2.0 authorization server for OSF in addition to its primary role as a CAS authentication server.

### The OAuth 2.0 Protocol

* [RFC 6749](https://tools.ietf.org/html/rfc6749)
* [OAuth 2.0](https://oauth.net/2/)
* [OAuth 2.0 Simplified](https://aaronparecki.com/oauth-2-simplified/)

### Parties

| Party | Who |
|----------------------|-------------------|
| Client Application | A web application |
| Authorization Server | OSF CAS |
| Resource Owner | OSF Users |
| Resource Server | OSF API |

## Features

### General

* Authorize client applications
* Exchange authorization code for access and refresh token
* Refresh access token using refresh token
* Revoke access and refresh tokens

### Client Application Owners

* Revoke all tokens of the application for all users

### Resource Owners

* List all authorized applications
* Revoke all tokens of an authorized application for the owner

<br>

## Endpoints

### `GET /oauth2/authorize`

#### Authorize a Client Application

Securely allows or denies the client application's request to access the resource user's information with specified scopes. It returns a one-time and short-lived authorization code, which will be used to follow up with the token-code exchange.

* Authorization Web Flow
* (1) The client application issues the initial authorization request to `oauth2/authorize` with the above parameters;
* (2) If the user is not logged in, redirects to the primary CAS login with `oauth2/callbackAuthorize` as service;
* (3) Both step 1 and 2 end up redirecting the user to `oauth2/callbackAuthorize` for service validation;
* (4) After validation, redirects to `oauth2/callbackAuthorize` one more time, which checks previous decisions and asks the user to allow or deny the authorization if necessary.
* (5) If denied, redirects to `/oauth2/callbackAuthorizeAction?action=DENY`; if allowed, redirects to `/oauth2/callbackAuthorizeAction?action=ALLOW`
* (6) Finally, for both decisions in step 5, redirects the user to the *Redirect URI*: `https://my.app.io/oauth2/callback?` with different query parameters as shown below.

* Request in Step (1)

```
https://accounts.osf.io/oauth2/authorize
```

| Parameter | Value / Example | Notes |
|-----------------|-------------------------------------|--------------------------------|
| response_type | code | |
| client_id | `ffe5247b810045a8a9277d3b3b4edc7a` | |
| redirect_uri | `https://my.app.io/oauth2/callback` | |
| scope | `osf.full_read` | |
| access_type | **`online`** / `offline` | Optional with default `online` |
| approval_prompt | **`auto`** / `force` | Optional with default `auto` |

* Response in Step (6)

| Parameter | Value / Example | Note |
|-----------|-----------------------------------------------------------|------------------------|
| code | `AC-1-mFs7MrWvaQy1fiidWGwXTw4dbAH30wk39cAELJnxizjGCUXYJl` | If `ALLOW` in step (5) |

| Parameter | Value / Example | Note |
|-----------|-----------------|-----------------------|
| error | `access_denied` | If `DENY` in step (5) |

<br><hr>

### `POST /oauth2/token`

#### Exchange Code for Token

Exchanges the authorization code for an access token and potentially a refresh token if `offline` mode was specified **when requesting for the authorization code**.

* Request

```
https://accounts.osf.io/oauth2/token
```

* `POST` Body Parameters

| Parameter | Value / Example | Notes |
|---------------|-----------------------------------------------------------|-------|
| code | `AC-1-mFs7MrWvaQy1fiidWGwXTw4dbAH30wk39cAELJnxizjGCUXYJl` | |
| client_id | `ffe5247b810045a8a9277d3b3b4edc7a` | |
| client_secret | `5PgE96R3Z53dBuwBDkJfbK6ItDXvGhaxYpQ6r4cU` | |
| redirect_uri | `https://my.app.io/oauth2/callback` | |
| grant_type | `authorization_code` | |

* Response Status

```
HTTP 200 OK
```
* Response Body if `online` Mode

```json
{
"access_token": "AT-2-p5jtVLATgft5EHqqbCTagg5i3q9e1htdcGEBvcpq0l1b2RyQav4bItEKPcDh94c5z7d7EK",
"token_type": "Bearer",
"expires_in": 3600
}
```

* Response Body if `offline` Mode

```json
{
"access_token": "AT-1-IBGuzWBdencAMz74LQkIuNcbLuu9WM3TYyacadkecrHUlcivs1GnWHjFmlkZPYg4TTAUM4",
"refresh_token": "RT-1-xfQXZaqXSQIJykCg2vnfdQjc5efVKdtteXaPo0OwCxWzIAacfC",
"token_type": "Bearer",
"expires_in": 3600
}
```

#### Refresh Access Token

In *`offline`* mode, the client application may request for a new access token by presenting the previously granted refresh token.

* Request

```
https://accounts.osf.io/oauth2/token
```

* `POST` Body Parameters

| Parameter | Value / Example | Note |
|---------------|-----------------------------------------------------------|------|
| refresh_token | `RT-1-xfQXZaqXSQIJykCg2vnfdQjc5efVKdtteXaPo0OwCxWzIAacfC` | |
| client_id | `ffe5247b810045a8a9277d3b3b4edc7a` | |
| client_secret | `5PgE96R3Z53dBuwBDkJfbK6ItDXvGhaxYpQ6r4cU` | |
| grant_type | `refresh_token` | |

* Response Status

```
HTTP 200 OK
```

* Response Body

```json
{
"access_token": "AT-3-WbBmXVTsPlhUatrs5sQmilVLnA30wVv3holmfFCbIfePRjzQ6UXCb7LwJHGbFqmad3wNXu",
"token_type": "Bearer",
"expires_in": 3600
}
```

<br><hr>

### `GET /oauth2/profile`

#### Profile

Provides the user's principal ID, any released attributes and a list of granted scopes.

* Request

```
https://accounts.osf.io/oauth2/profile
```

* Authorization Header

| Name | Value / Example | Note |
|---------------|--------------------------------------------------------------------------------------|------|
| Authorization | `Bearer AT-4-IdanI4hWiybRzARBiLrlMdeMTlDJIqo1UgVLb4MHzbF13pNIT5POrfQTMW5yEyVD1oXXcz` | |

* Response Status

```
HTTP 200 OK
```

* Response Body

```json
{
"scope": [
"osf.full_read"
],
"id": "f2t7d"
}
```

<br><hr>

### `POST /oauth2/revoke`

#### Revoke One Token

Handles revocation of refresh and access tokens.

* Request: Revoke one access token

```
https://accounts.osf.io/oauth2/revoke
```

* `POST` Body Parameters for revoke one access token

| Name | Value / Example | Note |
|-------|-------------------------------------------------------------------------------|------|
| token | `AT-6-0ckMxjkBHgs5PMqbCtg9BgFo49Y60A1bC5QxFnQeWdiWe9ZfvKwWS52jyIwLrrwVMGFxfa` | |

* `POST` Body Parameters for revoke one refresh token and all access token granted by this refresh token.

| Name | Value / Example | Note |
|-------|-----------------------------------------------------------|------|
| token | `RT-1-xfQXZaqXSQIJykCg2vnfdQjc5efVKdtteXaPo0OwCxWzIAacfC` | |

* Response Status

```
HTTP 204 NO CONTENT
```

#### Revoke Tokens for a Client Application

Revokes all tokens associated with a client application specified by the given client ID.

* Request

```
https://accounts.osf.io/oauth2/revoke
```

* `POST` Body Parameters

| Parameter | Value / Example | Note |
|---------------|--------------------------------------------|------|
| client_id | `ffe5247b810045a8a9277d3b3b4edc7a` | |
| client_secret | `5PgE96R3Z53dBuwBDkJfbK6ItDXvGhaxYpQ6r4cU` | |

* Response Status

```
HTTP 204 NO CONTENT
```

#### Revoke All Tokens for a Resource User

Revokes all tokens of a client application that have been issued to a resource user. The application is specified by the client ID and the user is specified by the principal ID associated with the access token. The token used for authorization must have been generated by the application *unless it is of token type `CAS`*.

* Request

```
https://accounts.osf.io/oauth2/revoke
```

* Authorization Header

| Name | Value / Example | Note |
|---------------|--------------------------------------------------------------------------------------|------|
| Authorization | `Bearer AT-7-PvVw9wIcTOZYXFCVWbFhwsf9Q3idASiJeBdiWmLExcXSG54lCycokgCefWsy2Nzds4LoAW` | |

* `POST` Body Parameters

| Parameter | Value / Example | Note |
|-----------|------------------------------------|------|
| client_id | `ffe5247b810045a8a9277d3b3b4edc7a` | |

* Response Status

```
HTTP 204 NO CONTENT
```
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,16 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfPostgresInte
if (oneTimePassword == null) {
throw new OneTimePasswordRequiredException("2FA TOTP required for user [" + username + "]");
}
final long transformedOneTimePassword = Long.parseLong(oneTimePassword);
boolean checkPassed;
try {
final long transformedOneTimePassword = Long.parseLong(oneTimePassword);
if (!TotpUtils.checkCode(osfTotp.getTotpSecretBase32(), transformedOneTimePassword)) {
throw new InvalidOneTimePasswordException("Invalid 2FA TOTP for user [" + username + "] (Type 1)");
}
} catch (final Exception e) {
checkPassed = TotpUtils.checkCode(osfTotp.getTotpSecretBase32(), transformedOneTimePassword);
} catch (final Exception e){
throw new InvalidOneTimePasswordException("Invalid 2FA TOTP for user [" + username + "] (Type 2)");
}
if (!checkPassed) {
throw new InvalidOneTimePasswordException("Invalid 2FA TOTP for user [" + username + "] (Type 1)");
}
}

if (!osfUser.isTermsOfServiceAccepted() && !isTermsOfServiceChecked) {
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/io/cos/cas/osf/model/OsfTotp.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.codec.binary.Base32;

Expand All @@ -28,6 +29,7 @@
@NoArgsConstructor
@Getter
@ToString
@Slf4j
public class OsfTotp extends AbstractOsfModel {

@OneToOne
Expand All @@ -50,8 +52,14 @@ private boolean isDeleted() {
}

public String getTotpSecretBase32() {
final byte[] bytes = DatatypeConverter.parseHexBinary(totpSecret);
return new Base32().encodeAsString(bytes);
try {
// Handle totpSecret generated before OSF Python 3.12 upgrade
final byte[] bytes = DatatypeConverter.parseHexBinary(totpSecret);
return new Base32().encodeAsString(bytes);
} catch (final IllegalArgumentException e) {
// Handle totpSecret generated after OSF Python 3.12 upgrade
return new Base32().encodeAsString(totpSecret.getBytes());
}
}

public boolean isActive() {
Expand Down

0 comments on commit 20a8606

Please sign in to comment.