Skip to content

Commit 20a8606

Browse files
committed
Merge branch 'release/24.1.0'
2 parents 32bb4fa + 38fd246 commit 20a8606

5 files changed

Lines changed: 312 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

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

5+
24.1.0 (08-09-2024)
6+
===================
7+
8+
* Updated 2FA secretparsing to support osf.io python 3.12 upgrade
9+
* Added OAuth guide
10+
* Fixed typo in Institution SSO guide
11+
512
23.2.1 (07-07-2023)
613
===================
714

README_INSTITUTION_SSO_INTEGRATION_GUIDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ For institutions that are not registered with any participant of eduGAIN, COS of
5252
* Required Attribute
5353
* Unique identifier for the user
5454
* User's institutional email
55-
* Either of the follwoing two
55+
* Either of the following two
5656
* User's full name
5757
* User's first **AND** last name
5858
* Attribute name and format
@@ -68,7 +68,7 @@ For institutions that are not registered with any participant of eduGAIN, COS of
6868

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

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

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

README_OAUTH_SSO_GUIDE.md

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
# OSF CAS as an OAuth Server
2+
3+
## About
4+
5+
OSF CAS serves as an OAuth 2.0 authorization server for OSF in addition to its primary role as a CAS authentication server.
6+
7+
### The OAuth 2.0 Protocol
8+
9+
* [RFC 6749](https://tools.ietf.org/html/rfc6749)
10+
* [OAuth 2.0](https://oauth.net/2/)
11+
* [OAuth 2.0 Simplified](https://aaronparecki.com/oauth-2-simplified/)
12+
13+
### Parties
14+
15+
| Party | Who |
16+
|----------------------|-------------------|
17+
| Client Application | A web application |
18+
| Authorization Server | OSF CAS |
19+
| Resource Owner | OSF Users |
20+
| Resource Server | OSF API |
21+
22+
## Features
23+
24+
### General
25+
26+
* Authorize client applications
27+
* Exchange authorization code for access and refresh token
28+
* Refresh access token using refresh token
29+
* Revoke access and refresh tokens
30+
31+
### Client Application Owners
32+
33+
* Revoke all tokens of the application for all users
34+
35+
### Resource Owners
36+
37+
* List all authorized applications
38+
* Revoke all tokens of an authorized application for the owner
39+
40+
<br>
41+
42+
## Endpoints
43+
44+
### `GET /oauth2/authorize`
45+
46+
#### Authorize a Client Application
47+
48+
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.
49+
50+
* Authorization Web Flow
51+
* (1) The client application issues the initial authorization request to `oauth2/authorize` with the above parameters;
52+
* (2) If the user is not logged in, redirects to the primary CAS login with `oauth2/callbackAuthorize` as service;
53+
* (3) Both step 1 and 2 end up redirecting the user to `oauth2/callbackAuthorize` for service validation;
54+
* (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.
55+
* (5) If denied, redirects to `/oauth2/callbackAuthorizeAction?action=DENY`; if allowed, redirects to `/oauth2/callbackAuthorizeAction?action=ALLOW`
56+
* (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.
57+
58+
* Request in Step (1)
59+
60+
```
61+
https://accounts.osf.io/oauth2/authorize
62+
```
63+
64+
| Parameter | Value / Example | Notes |
65+
|-----------------|-------------------------------------|--------------------------------|
66+
| response_type | code | |
67+
| client_id | `ffe5247b810045a8a9277d3b3b4edc7a` | |
68+
| redirect_uri | `https://my.app.io/oauth2/callback` | |
69+
| scope | `osf.full_read` | |
70+
| access_type | **`online`** / `offline` | Optional with default `online` |
71+
| approval_prompt | **`auto`** / `force` | Optional with default `auto` |
72+
73+
* Response in Step (6)
74+
75+
| Parameter | Value / Example | Note |
76+
|-----------|-----------------------------------------------------------|------------------------|
77+
| code | `AC-1-mFs7MrWvaQy1fiidWGwXTw4dbAH30wk39cAELJnxizjGCUXYJl` | If `ALLOW` in step (5) |
78+
79+
| Parameter | Value / Example | Note |
80+
|-----------|-----------------|-----------------------|
81+
| error | `access_denied` | If `DENY` in step (5) |
82+
83+
<br><hr>
84+
85+
### `POST /oauth2/token`
86+
87+
#### Exchange Code for Token
88+
89+
Exchanges the authorization code for an access token and potentially a refresh token if `offline` mode was specified **when requesting for the authorization code**.
90+
91+
* Request
92+
93+
```
94+
https://accounts.osf.io/oauth2/token
95+
```
96+
97+
* `POST` Body Parameters
98+
99+
| Parameter | Value / Example | Notes |
100+
|---------------|-----------------------------------------------------------|-------|
101+
| code | `AC-1-mFs7MrWvaQy1fiidWGwXTw4dbAH30wk39cAELJnxizjGCUXYJl` | |
102+
| client_id | `ffe5247b810045a8a9277d3b3b4edc7a` | |
103+
| client_secret | `5PgE96R3Z53dBuwBDkJfbK6ItDXvGhaxYpQ6r4cU` | |
104+
| redirect_uri | `https://my.app.io/oauth2/callback` | |
105+
| grant_type | `authorization_code` | |
106+
107+
* Response Status
108+
109+
```
110+
HTTP 200 OK
111+
```
112+
* Response Body if `online` Mode
113+
114+
```json
115+
{
116+
"access_token": "AT-2-p5jtVLATgft5EHqqbCTagg5i3q9e1htdcGEBvcpq0l1b2RyQav4bItEKPcDh94c5z7d7EK",
117+
"token_type": "Bearer",
118+
"expires_in": 3600
119+
}
120+
```
121+
122+
* Response Body if `offline` Mode
123+
124+
```json
125+
{
126+
"access_token": "AT-1-IBGuzWBdencAMz74LQkIuNcbLuu9WM3TYyacadkecrHUlcivs1GnWHjFmlkZPYg4TTAUM4",
127+
"refresh_token": "RT-1-xfQXZaqXSQIJykCg2vnfdQjc5efVKdtteXaPo0OwCxWzIAacfC",
128+
"token_type": "Bearer",
129+
"expires_in": 3600
130+
}
131+
```
132+
133+
#### Refresh Access Token
134+
135+
In *`offline`* mode, the client application may request for a new access token by presenting the previously granted refresh token.
136+
137+
* Request
138+
139+
```
140+
https://accounts.osf.io/oauth2/token
141+
```
142+
143+
* `POST` Body Parameters
144+
145+
| Parameter | Value / Example | Note |
146+
|---------------|-----------------------------------------------------------|------|
147+
| refresh_token | `RT-1-xfQXZaqXSQIJykCg2vnfdQjc5efVKdtteXaPo0OwCxWzIAacfC` | |
148+
| client_id | `ffe5247b810045a8a9277d3b3b4edc7a` | |
149+
| client_secret | `5PgE96R3Z53dBuwBDkJfbK6ItDXvGhaxYpQ6r4cU` | |
150+
| grant_type | `refresh_token` | |
151+
152+
* Response Status
153+
154+
```
155+
HTTP 200 OK
156+
```
157+
158+
* Response Body
159+
160+
```json
161+
{
162+
"access_token": "AT-3-WbBmXVTsPlhUatrs5sQmilVLnA30wVv3holmfFCbIfePRjzQ6UXCb7LwJHGbFqmad3wNXu",
163+
"token_type": "Bearer",
164+
"expires_in": 3600
165+
}
166+
```
167+
168+
<br><hr>
169+
170+
### `GET /oauth2/profile`
171+
172+
#### Profile
173+
174+
Provides the user's principal ID, any released attributes and a list of granted scopes.
175+
176+
* Request
177+
178+
```
179+
https://accounts.osf.io/oauth2/profile
180+
```
181+
182+
* Authorization Header
183+
184+
| Name | Value / Example | Note |
185+
|---------------|--------------------------------------------------------------------------------------|------|
186+
| Authorization | `Bearer AT-4-IdanI4hWiybRzARBiLrlMdeMTlDJIqo1UgVLb4MHzbF13pNIT5POrfQTMW5yEyVD1oXXcz` | |
187+
188+
* Response Status
189+
190+
```
191+
HTTP 200 OK
192+
```
193+
194+
* Response Body
195+
196+
```json
197+
{
198+
"scope": [
199+
"osf.full_read"
200+
],
201+
"id": "f2t7d"
202+
}
203+
```
204+
205+
<br><hr>
206+
207+
### `POST /oauth2/revoke`
208+
209+
#### Revoke One Token
210+
211+
Handles revocation of refresh and access tokens.
212+
213+
* Request: Revoke one access token
214+
215+
```
216+
https://accounts.osf.io/oauth2/revoke
217+
```
218+
219+
* `POST` Body Parameters for revoke one access token
220+
221+
| Name | Value / Example | Note |
222+
|-------|-------------------------------------------------------------------------------|------|
223+
| token | `AT-6-0ckMxjkBHgs5PMqbCtg9BgFo49Y60A1bC5QxFnQeWdiWe9ZfvKwWS52jyIwLrrwVMGFxfa` | |
224+
225+
* `POST` Body Parameters for revoke one refresh token and all access token granted by this refresh token.
226+
227+
| Name | Value / Example | Note |
228+
|-------|-----------------------------------------------------------|------|
229+
| token | `RT-1-xfQXZaqXSQIJykCg2vnfdQjc5efVKdtteXaPo0OwCxWzIAacfC` | |
230+
231+
* Response Status
232+
233+
```
234+
HTTP 204 NO CONTENT
235+
```
236+
237+
#### Revoke Tokens for a Client Application
238+
239+
Revokes all tokens associated with a client application specified by the given client ID.
240+
241+
* Request
242+
243+
```
244+
https://accounts.osf.io/oauth2/revoke
245+
```
246+
247+
* `POST` Body Parameters
248+
249+
| Parameter | Value / Example | Note |
250+
|---------------|--------------------------------------------|------|
251+
| client_id | `ffe5247b810045a8a9277d3b3b4edc7a` | |
252+
| client_secret | `5PgE96R3Z53dBuwBDkJfbK6ItDXvGhaxYpQ6r4cU` | |
253+
254+
* Response Status
255+
256+
```
257+
HTTP 204 NO CONTENT
258+
```
259+
260+
#### Revoke All Tokens for a Resource User
261+
262+
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`*.
263+
264+
* Request
265+
266+
```
267+
https://accounts.osf.io/oauth2/revoke
268+
```
269+
270+
* Authorization Header
271+
272+
| Name | Value / Example | Note |
273+
|---------------|--------------------------------------------------------------------------------------|------|
274+
| Authorization | `Bearer AT-7-PvVw9wIcTOZYXFCVWbFhwsf9Q3idASiJeBdiWmLExcXSG54lCycokgCefWsy2Nzds4LoAW` | |
275+
276+
* `POST` Body Parameters
277+
278+
| Parameter | Value / Example | Note |
279+
|-----------|------------------------------------|------|
280+
| client_id | `ffe5247b810045a8a9277d3b3b4edc7a` | |
281+
282+
* Response Status
283+
284+
```
285+
HTTP 204 NO CONTENT
286+
```

src/main/java/io/cos/cas/osf/authentication/handler/support/OsfPostgresAuthenticationHandler.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,16 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfPostgresInte
165165
if (oneTimePassword == null) {
166166
throw new OneTimePasswordRequiredException("2FA TOTP required for user [" + username + "]");
167167
}
168+
final long transformedOneTimePassword = Long.parseLong(oneTimePassword);
169+
boolean checkPassed;
168170
try {
169-
final long transformedOneTimePassword = Long.parseLong(oneTimePassword);
170-
if (!TotpUtils.checkCode(osfTotp.getTotpSecretBase32(), transformedOneTimePassword)) {
171-
throw new InvalidOneTimePasswordException("Invalid 2FA TOTP for user [" + username + "] (Type 1)");
172-
}
173-
} catch (final Exception e) {
171+
checkPassed = TotpUtils.checkCode(osfTotp.getTotpSecretBase32(), transformedOneTimePassword);
172+
} catch (final Exception e){
174173
throw new InvalidOneTimePasswordException("Invalid 2FA TOTP for user [" + username + "] (Type 2)");
175174
}
175+
if (!checkPassed) {
176+
throw new InvalidOneTimePasswordException("Invalid 2FA TOTP for user [" + username + "] (Type 1)");
177+
}
176178
}
177179

178180
if (!osfUser.isTermsOfServiceAccepted() && !isTermsOfServiceChecked) {

src/main/java/io/cos/cas/osf/model/OsfTotp.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.Getter;
55
import lombok.NoArgsConstructor;
66
import lombok.ToString;
7+
import lombok.extern.slf4j.Slf4j;
78

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

@@ -28,6 +29,7 @@
2829
@NoArgsConstructor
2930
@Getter
3031
@ToString
32+
@Slf4j
3133
public class OsfTotp extends AbstractOsfModel {
3234

3335
@OneToOne
@@ -50,8 +52,14 @@ private boolean isDeleted() {
5052
}
5153

5254
public String getTotpSecretBase32() {
53-
final byte[] bytes = DatatypeConverter.parseHexBinary(totpSecret);
54-
return new Base32().encodeAsString(bytes);
55+
try {
56+
// Handle totpSecret generated before OSF Python 3.12 upgrade
57+
final byte[] bytes = DatatypeConverter.parseHexBinary(totpSecret);
58+
return new Base32().encodeAsString(bytes);
59+
} catch (final IllegalArgumentException e) {
60+
// Handle totpSecret generated after OSF Python 3.12 upgrade
61+
return new Base32().encodeAsString(totpSecret.getBytes());
62+
}
5563
}
5664

5765
public boolean isActive() {

0 commit comments

Comments
 (0)