diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6adc6e6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,36 @@ +name: Build +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: ./gradlew build sonarqube --info diff --git a/CHANGELOG.md b/CHANGELOG.md index 60e67b0..510d48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. +21.0.0 (02-03-2020) +=================== + +OSF CAS third release with web flow updates, institution SSO, and FE rework + +* Login and logout web flow fixes and improvements +* Fully functional institution SSO, BE and FE +* FE rework and UI / UX improvements + +Extra features + +* Institution SSO migration +* TOS consent check +* SonarQube integraiton + 20.1.0 (11-05-2020) =================== diff --git a/Dockerfile-local b/Dockerfile-local index 52fb223..b9df6ca 100644 --- a/Dockerfile-local +++ b/Dockerfile-local @@ -13,12 +13,13 @@ RUN mkdir -p ~/.gradle \ && ./gradlew --version; RUN cd cas-overlay && ./gradlew clean build --parallel --no-daemon; -# The build process above in docker may takes a long time depending on your local resources. This OK if you only use +# The build process above in docker may take a long time depending on your local resources. This OK if you only use # CAS by building it once. For local development, building in local shell or with your IDE such as IntelliJ is much -# faster. Afterwards, simply comment out the above "RUN" command and enable the following "COPY" one. In fact, this -# stage can be skipped if you have the WAR built locally. Just need run the second stage with a modified WAR source. +# faster. Afterwards, simply comment out the above "RUN" command above and enable the "COPY" one below. # COPY ./build cas-overlay/build/ +# In fact, the above "overlay" stage can be skipped if you have the WAR built locally. Simply run this second stage +# "cas" with a modified WAR source. FROM adoptopenjdk/openjdk11:alpine-jre AS cas LABEL "Organization"="Center for Open Science" @@ -33,9 +34,9 @@ RUN cd / \ COPY etc/cas/ /etc/cas/ COPY etc/cas/config/ /etc/cas/config/ -# Use "cas-local.properties" and "log4j2-local.xml" for local development -RUN rm etc/cas/config/cas.properties +# Use "cas-local.properties", "instn-authn-local.xsl" and "log4j2-local.xml" for local development COPY etc/cas/config/local/cas-local.properties etc/cas/config/cas.properties +COPY etc/cas/config/local/instn-authn-local.xsl etc/cas/config/instn-authn.xsl COPY etc/cas/config/local/log4j2-local.xml etc/cas/config/log4j2.xml RUN rm -r etc/cas/config/local diff --git a/build.gradle b/build.gradle index 13358a9..96ce29f 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ buildscript { classpath "gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:${project.jibVersion}" classpath "io.freefair.gradle:maven-plugin:${project.gradleMavenPluginVersion}" classpath "io.freefair.gradle:lombok-plugin:${project.gradleLombokPluginVersion}" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1.1" } } @@ -58,8 +59,13 @@ def casWebApplicationBinaryName = "cas.war" project.ext."casServerVersion" = casServerVersion project.ext."casWebApplicationBinaryName" = casWebApplicationBinaryName +apply plugin: "war" apply plugin: "io.freefair.war-overlay" +apply from: rootProject.file("gradle/tasks.gradle") +apply from: rootProject.file("gradle/springboot.gradle") +apply from: rootProject.file("gradle/dockerjib.gradle") + apply plugin: "io.freefair.lombok" lombok { config["config.stopBubbling"] = "true" @@ -70,14 +76,30 @@ lombok { config["lombok.toString.doNotUseGetters"] = "true" } -apply from: rootProject.file("gradle/tasks.gradle") - -apply plugin: "war" apply plugin: "eclipse" +eclipse { + classpath { + downloadSources = true + downloadJavadoc = true + } +} + apply plugin: "idea" +idea { + module { + downloadJavadoc = true + downloadSources = true + } +} -apply from: rootProject.file("gradle/springboot.gradle") -apply from: rootProject.file("gradle/dockerjib.gradle") +apply plugin: "org.sonarqube" +sonarqube { + properties { + property "sonar.projectKey", "CenterForOpenScience_osf-cas" + property "sonar.organization", "centerforopenscience" + property "sonar.host.url", "https://sonarcloud.io" + } +} dependencies { // Other CAS dependencies/modules may be listed here... @@ -123,6 +145,12 @@ dependencies { // Google GSON implementation "com.google.code.gson:gson:${gsonVersion}" + + // Javascript Object Signing and Encryption (JOSE) and JSON Web Tokens (JWT) + implementation "com.nimbusds:nimbus-jose-jwt:${nimbusJoseVersion}" + + // Apache HttpComponents Client fluent API + implementation "org.apache.httpcomponents:fluent-hc:${fluentHcVersion}" } tasks.findByName("jibDockerBuild") @@ -146,17 +174,3 @@ configurations.all { } } } - -eclipse { - classpath { - downloadSources = true - downloadJavadoc = true - } -} - -idea { - module { - downloadJavadoc = true - downloadSources = true - } -} diff --git a/docker-reload.sh b/docker-reload.sh index 2a685c2..bed6b7f 100755 --- a/docker-reload.sh +++ b/docker-reload.sh @@ -1,18 +1,37 @@ -#!/bin/bash +#!/bin/zsh # Rebuild locally and replace the WAR +echo "######## Rebuild & Replace WAR ########" +echo "./gradlew clean build" ./gradlew clean build +echo "docker cp ./build/libs/cas.war cas:/cas-overlay" docker cp ./build/libs/cas.war cas:/cas-overlay +echo "################# Done ################" # Sync configuration files +echo "########## Sync Config Files ##########" +echo "docker exec -d cas sh -c \"rm -rf /etc/cas/config/*\"" docker exec -d cas sh -c "rm -rf /etc/cas/config/*" +echo "docker cp ./etc/cas/config/local/cas-local.properties cas:/etc/cas/config/cas.properties" docker cp ./etc/cas/config/local/cas-local.properties cas:/etc/cas/config/cas.properties +echo "docker cp ./etc/cas/config/local/instn-authn-local.xsl cas:/etc/cas/config/instn-authn.xsl" +docker cp ./etc/cas/config/local/instn-authn-local.xsl cas:/etc/cas/config/instn-authn.xsl +echo "docker cp ./etc/cas/config/local/log4j2-local.xml cas:/etc/cas/config/log4j2.xml" docker cp ./etc/cas/config/local/log4j2-local.xml cas:/etc/cas/config/log4j2.xml +echo "################# Done ################" # Sync JSON registered service files +echo "####### Sync Service Definition #######" +echo "docker exec -d cas sh -c \"rm -rf /etc/cas/services/*\"" docker exec -d cas sh -c "rm -rf /etc/cas/services/*" +echo "docker cp ./etc/cas/services/local/. cas:/etc/cas/services" docker cp ./etc/cas/services/local/. cas:/etc/cas/services +echo "################# Done ################" # Restart the container +echo "########## Restart Container ##########" +echo "docker restart cas" docker restart cas +echo "docker logs -f --tail 0 cas" +echo "####### OSF CAS LOG STARTS HERE #######" docker logs -f --tail 0 cas diff --git a/etc/cas/config/cas.properties b/etc/cas/config/cas.properties index c014f20..a89ab70 100644 --- a/etc/cas/config/cas.properties +++ b/etc/cas/config/cas.properties @@ -15,12 +15,6 @@ cas.server.prefix=${cas.server.name} # Tomcat Server # cas.server.tomcat.server-name=OSF CAS -# Enable additional HTTP connections for the embedded Tomcat container (when SSL is enabled by default) -# cas.server.tomcat.http.port=${TOMCAT_HTTP_PORT:80} -# cas.server.tomcat.http.protocol=org.apache.coyote.http11.Http11NioProtocol -# cas.server.tomcat.http.enabled=true -# cas.server.tomcat.http.attributes= -# e.g. cas.server.tomcat.http.attributes.{attribute-name}={attributeValue} ######################################################################################################################## ######################################################################################################################## @@ -64,6 +58,39 @@ cas.logout.confirm-logout=false cas.logout.remove-descendant-tickets=false ######################################################################################################################## +######################################################################################################################## +# OSF URLs +######################################################################################################################## +# OSF +# +cas.authn.osf-url.home=https://{{ .Values.osfDomain }}/ +cas.authn.osf-url.dashboard=https://{{ .Values.osfDomain }}/dashboard/ +cas.authn.osf-url.login-with-next=https://{{ .Values.osfDomain }}/login?next= +cas.authn.osf-url.logout=https://{{ .Values.osfDomain }}/logout/ +cas.authn.osf-url.resend-confirmation=https://{{ .Values.osfDomain }}/resend/ +cas.authn.osf-url.forgot-password=https://{{ .Values.osfDomain }}/forgotpassword/ +cas.authn.osf-url.forgot-password-institution=https://{{ .Values.osfDomain }}/forgotpassword-institution/ +cas.authn.osf-url.register=https://{{ .Values.osfDomain }}/register/ +cas.authn.osf-url.institutions-home=https://{{ .Values.osfDomain }}/institutions/ +######################################################################################################################## + +######################################################################################################################## +# OSF API Settings for Institution Authentication +######################################################################################################################## +# Authentication Endpoint +# +cas.authn.osf-api.instn-authn-endpoint=https://{{ .Values.apiDomain }}/v2/institutions/auth/ +# +# JWT / JWE secrets for signing and encrypting authentication request payload +# +cas.authn.osf-api.instn-authn-jwt-secret=${OSF_JWT_SECRET} +cas.authn.osf-api.instn-authn-jwe-secret=${OSF_JWE_SECRET} +# +# Path of the XSL file for parsing and transforming XML authentication responses +# +cas.authn.osf-api.instn-authn-xsl-location=file:/etc/cas/institutions-auth.xsl +######################################################################################################################## + ######################################################################################################################## # OSF PostgreSQL Authentication # See: https://apereo.github.io/cas/6.2.x/installation/Configuring-Custom-Authentication.html @@ -91,8 +118,6 @@ cas.authn.osf-postgres.jpa.dialect=${OSF_DB_HIBERNATE_DIALECT:io.cos.cas.osf.hib cas.jdbc.show-sql=false cas.jdbc.gen-ddl=true cas.jdbc.case-insensitive=false -# cas.jdbc.physical-table-names= -# e.g. cas.jdbc.physical-table-names.{table-name}={new-table-name} # # General JPA Settings # @@ -204,13 +229,13 @@ cas.authn.pac4j.orcid.callback-url-type=QUERY_PARAMETER # # Delegation Client: CAS # -cas.authn.pac4j.cas[0].login-url=https://accounts.staging.osf.io/login -cas.authn.pac4j.cas[0].client-name=stage1cas +cas.authn.pac4j.cas[0].login-url=https://bprdeis.cord.edu:8443/cas/login +cas.authn.pac4j.cas[0].client-name=cord cas.authn.pac4j.cas[0].protocol=SAML cas.authn.pac4j.cas[0].callback-url-type=QUERY_PARAMETER # -cas.authn.pac4j.cas[1].login-url=https://accounts.staging2.osf.io/login -cas.authn.pac4j.cas[1].client-name=stage2cas -cas.authn.pac4j.cas[1].protocol=CAS30 +cas.authn.pac4j.cas[1].login-url=https://stwcas.okstate.edu/cas/login +cas.authn.pac4j.cas[1].client-name=okstate +cas.authn.pac4j.cas[1].protocol=SAML cas.authn.pac4j.cas[1].callback-url-type=QUERY_PARAMETER ######################################################################################################################## diff --git a/etc/cas/config/instn-authn.xsl b/etc/cas/config/instn-authn.xsl new file mode 100644 index 0000000..19bdf14 --- /dev/null +++ b/etc/cas/config/instn-authn.xsl @@ -0,0 +1,982 @@ + + + + + + + + + + + + + + + + + + + + asu + + + + + + + + + + + + + + + + + + + + + bt + + + + + + + + + + + + + + + + + + + + bu + + + + + + + + + + + + + + + + + + + + brown + + + + + + + + + + + + + + + + + + + + callutheran + + + + + + + + + + + + + + + + cmu + + + + + + + + + + + + + + + + + + + + cornell + + + + + + + + + + + + + + + + + + + + cwru + + + + + + + + + + + + + + + + + + + + duke + + + + + + + + + + + + + + + + + + + + ecu + + + + + + + + + + + + + + + + + + + + ferris + + + + + + + + + + + + + + + + + + fsu + + + + + + + + + + + + + + + + gmu + + + + + + + + + + + + + + + + + + + + gwu + + + + + + + + + + + + + + + + + + + + iit + + + + + + + + + + + + + + + + + + + + itb + + + + + + + + + + + + + + + + + + + + jmu + + + + + + + + + + + + + + + + + + + + jhu + + + + + + + + + + + + + + + + mit + + + + + + + + + + + + + + + + + + + + mq + + + + + + + + + + + + + + + + + + + + nyu + + + + + + + + + + + + + + + + ou + + + + + + + + + + + + + + + + + + + + csic + + + + + + + + + + + + + + + + temple + + + + + + + + + + + + + + + + + + + + tufts + + + + + + + + + + + + + + + + + + + + ugent + + + + + + + + + + + + + + + + + + + + ua + + + + + + + + + + + + + + + + + + + + ubc + + + + + + + + + + + + + + + + ucla + + + + + + + + + + + + + + + + + + + + ucsd + + + + + + + + + + + + + + + + ucr + + + + + + + + + + + + + + + + + + + + uct + + + + + + + + + + + + + + + + + + + + uc + + + + + + + + + + + + + + + + colorado + + + + + + + + + + + + + + + + ugoe + + + + + + + + + + + + + + + + + + + + universityofkent + + + + + + + + + + + + + + + + mq + + + + + + + + + + + + + + + + + + + + unc + + + + + + + + + + + + + + + + + + + + nd + + + + + + + + + + + + + + + + usc + + + + + + + + + + + + + + + + + + + + + + sc + + + + + + + + + + + + + + + + + + + + utdallas + + + + + + + + + + + + + + + + + + + + uva + + + + + + + + + + + + + + + + uw + + + + + + + + + + + + + + + + uwstout + + + + + + + + + + + + + + + + + + + + vcu + + + + + + + + + + + + + + + + vt + + + + + + + + + + + + + + + + wustl + + + + + + + + + + + + + + + + + + + + Error: Unknown Identity Provider '' + + + + + + + + + + + + + cord + + + + + + + + + + + + + + + + okstate + + + + + + + + + + + + + + + + + + Error: Unknown Identity Provider '' + + + + + + Error: Unknown Delegation Protocol '' + + + + + + diff --git a/etc/cas/config/local/cas-local.properties b/etc/cas/config/local/cas-local.properties index 3e3cc6d..72ee056 100644 --- a/etc/cas/config/local/cas-local.properties +++ b/etc/cas/config/local/cas-local.properties @@ -65,6 +65,39 @@ cas.logout.confirm-logout=false cas.logout.remove-descendant-tickets=false ######################################################################################################################## +######################################################################################################################## +# OSF URLs +######################################################################################################################## +# OSF +# +cas.authn.osf-url.home=http://localhost:5000/ +cas.authn.osf-url.dashboard=http://localhost:5000/dashboard/ +cas.authn.osf-url.login-with-next=http://localhost:5000/login?next= +cas.authn.osf-url.logout=http://localhost:5000/logout/ +cas.authn.osf-url.resend-confirmation=http://localhost:5000/resend/ +cas.authn.osf-url.forgot-password=http://localhost:5000/forgotpassword/ +cas.authn.osf-url.forgot-password-institution=http://localhost:5000/forgotpassword-institution/ +cas.authn.osf-url.register=http://localhost:5000/register/ +cas.authn.osf-url.institutions-home=http://localhost:5000/institutions/ +######################################################################################################################## + +######################################################################################################################## +# OSF API Settings for Institution Authentication +######################################################################################################################## +# Authentication Endpoint +# +cas.authn.osf-api.instn-authn-endpoint=http://192.168.168.167:8000/v2/institutions/auth/ +# +# JWT / JWE secrets for signing and encrypting authentication request payload +# +cas.authn.osf-api.instn-authn-jwt-secret=osf_api_cas_login_jwt_secret_32b +cas.authn.osf-api.instn-authn-jwe-secret=osf_api_cas_login_jwe_secret_32b +# +# Path of the XSL file for parsing and transforming XML authentication responses +# +cas.authn.osf-api.instn-authn-xsl-location=file:/etc/cas/config/instn-authn.xsl +######################################################################################################################## + ######################################################################################################################## # OSF PostgreSQL Authentication # See: https://apereo.github.io/cas/6.2.x/installation/Configuring-Custom-Authentication.html @@ -206,14 +239,14 @@ cas.authn.pac4j.orcid.callback-url-type=QUERY_PARAMETER # # Delegation Client: CAS # -cas.authn.pac4j.cas[0].login-url=https://accounts.staging.osf.io/login -cas.authn.pac4j.cas[0].client-name=stage1cas +cas.authn.pac4j.cas[0].login-url=https://bprdeis.cord.edu:8443/cas/login +cas.authn.pac4j.cas[0].client-name=cord cas.authn.pac4j.cas[0].protocol=SAML cas.authn.pac4j.cas[0].callback-url-type=QUERY_PARAMETER # -cas.authn.pac4j.cas[1].login-url=https://accounts.staging2.osf.io/login -cas.authn.pac4j.cas[1].client-name=stage2cas -cas.authn.pac4j.cas[1].protocol=CAS30 +cas.authn.pac4j.cas[1].login-url=https://stwcas.okstate.edu/cas/login +cas.authn.pac4j.cas[1].client-name=okstate +cas.authn.pac4j.cas[1].protocol=SAML cas.authn.pac4j.cas[1].callback-url-type=QUERY_PARAMETER # cas.authn.pac4j.cas[2].login-url=http://192.168.168.167:8081/login diff --git a/etc/cas/config/local/instn-authn-local.xsl b/etc/cas/config/local/instn-authn-local.xsl new file mode 100644 index 0000000..4cba204 --- /dev/null +++ b/etc/cas/config/local/instn-authn-local.xsl @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + esu + + + + + + + + + + + + + brown + + + + + + + + + + + + + + + + + + + + ua + + + + + + + + + + + + + + + + + + + + Error: Unknown Identity Provider '' + + + + + + + + + + + ecu + + + + + + + + + + + + fakecas + + + + + + + + + + + + + Error: Unknown Identity Provider '' + + + + + + Error: Unknown Delegation Protocol '' + + + + diff --git a/etc/cas/services/local/cas-203948234207100.json b/etc/cas/services/local/cas-203948234207100.json index 27fc8ff..9c12c74 100644 --- a/etc/cas/services/local/cas-203948234207100.json +++ b/etc/cas/services/local/cas-203948234207100.json @@ -1,8 +1,8 @@ { "@class": "org.apereo.cas.services.RegexRegisteredService", "serviceId": "^https?://(localhost|127\\.0\\.0\\.1|192\\.168\\.168\\.167)(|:8080|:8443)/.*", - "name": "OSF CAS", - "description": "OSF CAS is the centralized authentication and authorization service for OSF", + "name": "", + "description": "", "id": 203948234207100, "evaluationOrder": 10, "logo": "/images/osf-banner.png", diff --git a/etc/cas/services/local/fakeosf-203948234207210.json b/etc/cas/services/local/cas-203948234207101.json similarity index 67% rename from etc/cas/services/local/fakeosf-203948234207210.json rename to etc/cas/services/local/cas-203948234207101.json index 4500aab..3328407 100644 --- a/etc/cas/services/local/fakeosf-203948234207210.json +++ b/etc/cas/services/local/cas-203948234207101.json @@ -1,11 +1,11 @@ { "@class": "org.apereo.cas.services.RegexRegisteredService", - "serviceId": "^https?://(localhost|127\\.0\\.0\\.1|192\\.168\\.168\\.167):5000/fake/.*", - "name": "One Special Fake OSF", - "description": "This is the One Special Fake OSF for testing hard-to-reach corner cases and errors (local-dev only)", - "id": 203948234207210, - "evaluationOrder": 25, - "logo": "/images/osf-logo.png", + "serviceId": "^/(login|logout)\\?.*", + "name": "", + "description": "", + "id": 203948234207101, + "evaluationOrder": 10, + "logo": "/images/osf-banner.png", "attributeReleasePolicy": { "@class": "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy", "allowedAttributes": [ diff --git a/etc/cas/services/local/casarxiv-203948234207239.json b/etc/cas/services/local/casarxiv-203948234207239.json new file mode 100644 index 0000000..dbbc71a --- /dev/null +++ b/etc/cas/services/local/casarxiv-203948234207239.json @@ -0,0 +1,36 @@ +{ + "@class": "org.apereo.cas.services.RegexRegisteredService", + "serviceId" : "^https?://(localhost|127\\.0\\.0\\.1):5000/(login|logout)/?\\?next=https?(%3A|:)(%2F|/)(%2F|/)((local\\.casarxiv\\.org(%3A|:)4200)|((localhost|127\\.0\\.0\\.1)(%3A|:)5000(%2F|/)preprints(%2F|/)casarxiv))($|%2F|/).*", + "name" : "CasArxiv Preprints", + "description" : "", + "id" : 203948234207239, + "evaluationOrder": 10, + "logo" : "/images/branded/preprints-casarxiv-logo.png", + "attributeReleasePolicy": { + "@class": "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy", + "allowedAttributes": [ + "java.util.ArrayList", + [ + "givenName", + "familyName", + "username" + ] + ] + }, + "accessStrategy": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy", + "delegatedAuthenticationPolicy": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceDelegatedAuthenticationPolicy", + "allowedProviders": [ + "java.util.ArrayList", + [ + "cord", + "fakecas", + "okstate", + "orcid" + ] + ], + "permitUndefined": false + } + } +} diff --git a/etc/cas/services/local/legacyosf-203948234207220.json b/etc/cas/services/local/legacyosf-203948234207220.json index 57027e7..8481090 100644 --- a/etc/cas/services/local/legacyosf-203948234207220.json +++ b/etc/cas/services/local/legacyosf-203948234207220.json @@ -1,8 +1,8 @@ { "@class": "org.apereo.cas.services.RegexRegisteredService", "serviceId": "^https?://(localhost|127\\.0\\.0\\.1|192\\.168\\.168\\.167):5000/.*", - "name": "OSF", - "description": "Manage and share your research with OSF – an free, open, easy and integrated platform to support your research and enable collaboration.", + "name": "", + "description": "", "id": 203948234207220, "evaluationOrder": 25, "logo": "/images/osf-logo.png", diff --git a/etc/cas/services/local/osf-203948234207230.json b/etc/cas/services/local/osf-203948234207230.json index 2680c8a..953c0ed 100644 --- a/etc/cas/services/local/osf-203948234207230.json +++ b/etc/cas/services/local/osf-203948234207230.json @@ -2,7 +2,7 @@ "@class": "org.apereo.cas.services.RegexRegisteredService", "serviceId": "^https?://(localhost|127\\.0\\.0\\.1|192\\.168\\.168\\.167):5000/(login|logout)/?\\?next=.*", "name": "", - "description": "There's a better way to manage your research! OSF is a free, open platform to support your research and enable collaboration.", + "description": "", "id": 203948234207230, "evaluationOrder": 20, "logo": "/images/osf-banner.png", @@ -24,6 +24,9 @@ "allowedProviders": [ "java.util.ArrayList", [ + "cord", + "fakecas", + "okstate", "orcid" ] ], diff --git a/etc/cas/services/local/preprints-203948234207240.json b/etc/cas/services/local/preprints-203948234207240.json index 506c33d..5928bc2 100644 --- a/etc/cas/services/local/preprints-203948234207240.json +++ b/etc/cas/services/local/preprints-203948234207240.json @@ -2,7 +2,7 @@ "@class": "org.apereo.cas.services.RegexRegisteredService", "serviceId": "^https?://(localhost|127\\.0\\.0\\.1):5000/(login|logout)/?\\?next=https?(%3A|:)(%2F|/)(%2F|/)(localhost|127\\.0\\.0\\.1)(%3A|:)5000(%2F|/)preprints($|%2F|/).*", "name": "", - "description": "Accelerating scholarly review, publishing, and discovery.", + "description": "", "id": 203948234207240, "evaluationOrder": 15, "logo": "/images/osf-preprints-banner.png", @@ -24,6 +24,9 @@ "allowedProviders": [ "java.util.ArrayList", [ + "cord", + "fakecas", + "okstate", "orcid" ] ], diff --git a/etc/cas/services/local/registries-203948234207340.json b/etc/cas/services/local/registries-203948234207340.json index 07ce061..160d223 100644 --- a/etc/cas/services/local/registries-203948234207340.json +++ b/etc/cas/services/local/registries-203948234207340.json @@ -2,7 +2,7 @@ "@class": "org.apereo.cas.services.RegexRegisteredService", "serviceId": "^https?://(localhost|127\\.0\\.0\\.1):5000/(login|logout)/?\\?next=https?(%3A|:)(%2F|/)(%2F|/)(localhost|127\\.0\\.0\\.1)(%3A|:)5000(%2F|/)registries($|%2F|/).*", "name": "", - "description": "The open registries network", + "description": "", "id": 203948234207340, "evaluationOrder": 15, "logo": "/images/osf-registries-banner.png", @@ -24,6 +24,9 @@ "allowedProviders": [ "java.util.ArrayList", [ + "cord", + "fakecas", + "okstate", "orcid" ] ], diff --git a/gradle.properties b/gradle.properties index 948c5eb..57abfbd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,3 +30,5 @@ allowInsecureRegistries=false gsonVersion=2.8.6 hibernateCoreVersion=5.4.21.Final springBootTomcatVersion=9.0.37 +nimbusJoseVersion=8.20.1 +fluentHcVersion=4.5.12 diff --git a/postman/osf-cas-shib-saml-institution-sso-test.json b/postman/osf-cas-shib-saml-institution-sso-test.json new file mode 100644 index 0000000..739f6f6 --- /dev/null +++ b/postman/osf-cas-shib-saml-institution-sso-test.json @@ -0,0 +1,222 @@ +{ + "info": { + "_postman_id": "91347496-1c7f-4584-a7b0-739afc51cf6a", + "name": "OSF Institution Shib-SAML SSO", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Brown University User", + "request": { + "method": "GET", + "header": [ + { + "key": "AUTH-mail", + "value": "chen@brown.edu", + "type": "text" + }, + { + "key": "AUTH-displayName", + "value": "Longzebrown Chenbrown", + "type": "text" + }, + { + "key": "AUTH-givenName", + "value": "Longzebrown", + "type": "text" + }, + { + "key": "AUTH-sn", + "value": "Chenbrown", + "type": "text" + }, + { + "key": "REMOTE_USER", + "value": "chenbrown123", + "type": "text" + }, + { + "key": "AUTH-Shib-Session-ID", + "value": "1234567812345678", + "type": "text" + }, + { + "key": "AUTH-Shib-Identity-Provider", + "value": "https://sso.brown.edu/idp/shibboleth", + "type": "text" + } + ], + "url": { + "raw": "http://192.168.168.167:8080/login", + "protocol": "http", + "host": [ + "192", + "168", + "168", + "167" + ], + "port": "8080", + "path": [ + "login" + ] + } + }, + "response": [] + }, + { + "name": "The Policy Lab User", + "request": { + "method": "GET", + "header": [ + { + "key": "AUTH-mail", + "value": "chen@policylab.io", + "type": "text" + }, + { + "key": "AUTH-displayName", + "value": "Longzepolicylab Chenpolicylab", + "type": "text" + }, + { + "key": "AUTH-givenName", + "value": "Longzepolicylab", + "type": "text" + }, + { + "key": "AUTH-sn", + "value": "Chenpolicylab", + "type": "text" + }, + { + "key": "REMOTE_USER", + "value": "chenpolicylab123", + "type": "text" + }, + { + "key": "AUTH-Shib-Session-ID", + "value": "1234567812345678", + "type": "text" + }, + { + "key": "AUTH-Shib-Identity-Provider", + "value": "https://sso.brown.edu/idp/shibboleth", + "type": "text" + }, + { + "key": "AUTH-isMemberOf", + "value": "thepolicylab", + "type": "text" + } + ], + "url": { + "raw": "http://192.168.168.167:8080/login", + "protocol": "http", + "host": [ + "192", + "168", + "168", + "167" + ], + "port": "8080", + "path": [ + "login" + ] + } + }, + "response": [] + }, + { + "name": "University of Arizona User", + "request": { + "method": "GET", + "header": [ + { + "key": "AUTH-mail", + "type": "text", + "value": "chen@ua.edu" + }, + { + "key": "AUTH-displayName", + "type": "text", + "value": "Longzeua Chenua" + }, + { + "key": "AUTH-givenName", + "type": "text", + "value": "Longzeua" + }, + { + "key": "AUTH-sn", + "type": "text", + "value": "Chenua" + }, + { + "key": "REMOTE_USER", + "type": "text", + "value": "chenua12" + }, + { + "key": "AUTH-Shib-Session-ID", + "type": "text", + "value": "1234567812345678" + }, + { + "key": "AUTH-Shib-Identity-Provider", + "type": "text", + "value": "urn:mace:incommon:arizona.edu" + }, + { + "key": "AUTH-departmentRaw", + "value": "Department of Computer Science", + "type": "text" + } + ], + "url": { + "raw": "http://192.168.168.167:8080/login", + "protocol": "http", + "host": [ + "192", + "168", + "168", + "167" + ], + "port": "8080", + "path": [ + "login" + ] + } + }, + "response": [] + }, + { + "name": "Clear CAS Session", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://192.168.168.167:8080/logout?service=http%3A%2F%2Flocalhost%3A5000%2Flogin%2F%3Fnext%3Dhttp%253A%252F%252Flocalhost%253A5000%252F", + "protocol": "http", + "host": [ + "192", + "168", + "168", + "167" + ], + "port": "8080", + "path": [ + "logout" + ], + "query": [ + { + "key": "service", + "value": "http%3A%2F%2Flocalhost%3A5000%2Flogin%2F%3Fnext%3Dhttp%253A%252F%252Flocalhost%253A5000%252F" + } + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} +} diff --git a/src/main/java/io/cos/cas/osf/authentication/credential/OsfPostgresCredential.java b/src/main/java/io/cos/cas/osf/authentication/credential/OsfPostgresCredential.java index 52f27ed..9027ecd 100644 --- a/src/main/java/io/cos/cas/osf/authentication/credential/OsfPostgresCredential.java +++ b/src/main/java/io/cos/cas/osf/authentication/credential/OsfPostgresCredential.java @@ -38,9 +38,11 @@ public class OsfPostgresCredential extends RememberMeUsernamePasswordCredential public static String AUTHENTICATION_ATTRIBUTE_REMEMBER_ME = "rememberMe"; - private static String DEFAULT_INSTITUTION_ID = "none_osf"; + public static String AUTHENTICATION_ATTRIBUTE_TOS_CONSENT = "termsOfServiceChecked"; - private static DelegationProtocol DEFAULT_DELEGATION_PROTOCOL = DelegationProtocol.NONE_OSF; + private static String DEFAULT_INSTITUTION_ID = "none"; + + private static DelegationProtocol DEFAULT_DELEGATION_PROTOCOL = DelegationProtocol.NONE; /** * The one-time and ephemeral OSF verification key. @@ -52,12 +54,34 @@ public class OsfPostgresCredential extends RememberMeUsernamePasswordCredential */ private String oneTimePassword; + /** + * The boolean flag that indicates whether the user has checked the terms of service consent agreement + */ + private boolean termsOfServiceChecked; + + /** + * The boolean flag that indicates successful delegated authentication if true. + */ private boolean remotePrincipal = Boolean.FALSE; + /** + * The ID that indicates which institution it is after successful authentication between CAS / Shib and institutions. + */ private String institutionId = DEFAULT_INSTITUTION_ID; + /** + * The user's institutional identity when authenticated via institutional SSO. + */ + private String institutionalIdentity = ""; + + /** + * The authentication delegation protocol that is used between CAS / Shib and institutions. + */ private DelegationProtocol delegationProtocol = DEFAULT_DELEGATION_PROTOCOL; + /** + * The authentication attributes that are parsed from raw authentication response. + */ private Map delegationAttributes = new LinkedHashMap<>(); @Override diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoNotImplementedException.java b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoFailedException.java similarity index 53% rename from src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoNotImplementedException.java rename to src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoFailedException.java index 85b6090..3d6a673 100644 --- a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoNotImplementedException.java +++ b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoFailedException.java @@ -5,25 +5,25 @@ import javax.security.auth.login.AccountException; /** - * Describes an authentication error condition where institution SSO is not implemented. + * Describes an authentication error condition where institution SSO has failed. * * @author Longze Chen - * @since 20.0.0 + * @since 21.0.0 */ @NoArgsConstructor -public class InstitutionSsoNotImplementedException extends AccountException { +public class InstitutionSsoFailedException extends AccountException { /** * Serialization metadata. */ - private static final long serialVersionUID = 5379752379314379863L; + private static final long serialVersionUID = 6977786012016534260L; /** - * Instantiates a new {@link InstitutionSsoNotImplementedException}. + * Instantiates a new {@link InstitutionSsoFailedException}. * * @param msg the msg */ - public InstitutionSsoNotImplementedException(final String msg) { + public InstitutionSsoFailedException(final String msg) { super(msg); } } diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/TermsOfServiceConsentRequiredException.java b/src/main/java/io/cos/cas/osf/authentication/exception/TermsOfServiceConsentRequiredException.java new file mode 100644 index 0000000..59b8826 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/authentication/exception/TermsOfServiceConsentRequiredException.java @@ -0,0 +1,29 @@ +package io.cos.cas.osf.authentication.exception; + +import lombok.NoArgsConstructor; + +import javax.security.auth.login.AccountException; + +/** + * Describes an authentication error condition where a user account needs to agree to OSF's terms of service. + * + * @author Longze Chen + * @since 21.0.0 + */ +@NoArgsConstructor +public class TermsOfServiceConsentRequiredException extends AccountException { + + /** + * Serialization metadata. + */ + private static final long serialVersionUID = -7702088330316457626L; + + /** + * Instantiates a new {@link TermsOfServiceConsentRequiredException}. + * + * @param msg the msg + */ + public TermsOfServiceConsentRequiredException(final String msg) { + super(msg); + } +} diff --git a/src/main/java/io/cos/cas/osf/authentication/handler/support/OsfPostgresAuthenticationHandler.java b/src/main/java/io/cos/cas/osf/authentication/handler/support/OsfPostgresAuthenticationHandler.java index e77d604..52db8ae 100644 --- a/src/main/java/io/cos/cas/osf/authentication/handler/support/OsfPostgresAuthenticationHandler.java +++ b/src/main/java/io/cos/cas/osf/authentication/handler/support/OsfPostgresAuthenticationHandler.java @@ -3,12 +3,12 @@ import io.cos.cas.osf.authentication.credential.OsfPostgresCredential; import io.cos.cas.osf.authentication.exception.AccountNotConfirmedIdpException; import io.cos.cas.osf.authentication.exception.AccountNotConfirmedOsfException; -import io.cos.cas.osf.authentication.exception.InstitutionSsoNotImplementedException; import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException; import io.cos.cas.osf.authentication.exception.InvalidPasswordException; import io.cos.cas.osf.authentication.exception.InvalidUserStatusException; import io.cos.cas.osf.authentication.exception.OneTimePasswordRequiredException; import io.cos.cas.osf.authentication.exception.InvalidVerificationKeyException; +import io.cos.cas.osf.authentication.exception.TermsOfServiceConsentRequiredException; import io.cos.cas.osf.authentication.support.DelegationProtocol; import io.cos.cas.osf.authentication.support.OsfUserStatus; import io.cos.cas.osf.authentication.support.OsfUserUtils; @@ -111,6 +111,7 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfPostgresInte final String oneTimePassword = credential.getOneTimePassword(); final String institutionId = credential.getInstitutionId(); final boolean isRememberMe = credential.isRememberMe(); + final boolean isTermsOfServiceChecked = credential.isTermsOfServiceChecked(); final boolean isRemotePrincipal = credential.isRemotePrincipal(); final DelegationProtocol delegationProtocol = credential.getDelegationProtocol(); @@ -127,12 +128,6 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfPostgresInte delegationProtocol ); - if (isRemotePrincipal) { - throw new InstitutionSsoNotImplementedException( - "Institution SSO not implemented for user [" + username + "] @ [" + institutionId +"]" - ); - } - final OsfUser osfUser = jpaOsfDao.findOneUserByEmail(username); if (osfUser == null) { throw new AccountNotFoundException("User [" + username + "] not found"); @@ -143,16 +138,21 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfPostgresInte } final String userStatus = OsfUserUtils.verifyUserStatus(osfUser); - if (plainTextPassword != null) { - if (!OsfPasswordUtils.verifyPassword(plainTextPassword, osfUser.getPassword())) { - throw new InvalidPasswordException("Invalid password for user [" + username + "]"); - } - } else if (verificationKey != null) { - if (!verificationKey.equals(osfUser.getVerificationKey())) { - throw new InvalidVerificationKeyException("Invalid verification key for user [" + username + "]"); - } + if (isRemotePrincipal) { + LOGGER.info("Skip password and verification key check for institution SSO [" + username + "] @ [" + institutionId +"]"); } else { - throw new FailedLoginException("Missing credential for user [" + username + "]"); + if (plainTextPassword != null) { + if (!OsfPasswordUtils.verifyPassword(plainTextPassword, osfUser.getPassword())) { + throw new InvalidPasswordException("Invalid password for user [" + username + "]"); + } + } else if (verificationKey != null) { + if (!verificationKey.equals(osfUser.getVerificationKey())) { + throw new InvalidVerificationKeyException("Invalid verification key for user [" + username + "]"); + } + } else { + LOGGER.info("Missing credential for user [" + username + "] @ [" + institutionId +"]"); + throw new FailedLoginException("Missing credential for user [" + username + "]"); + } } final OsfTotp osfTotp = jpaOsfDao.findOneTotpByOwnerId(osfUser.getId()); @@ -170,6 +170,11 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfPostgresInte } } + if (!osfUser.isTermsOfServiceAccepted() && !isTermsOfServiceChecked) { + LOGGER.info("Terms of service consent is required for [" + username + "]"); + throw new TermsOfServiceConsentRequiredException("Terms of service consent is required for [" + username + "]"); + } + if (OsfUserStatus.USER_NOT_CONFIRMED_OSF.equals(userStatus)) { throw new AccountNotConfirmedOsfException( "User [" + username + "] is registered via OSF but not confirmed" diff --git a/src/main/java/io/cos/cas/osf/authentication/metadata/OsfPostgresAuthenticationMetaDataPopulator.java b/src/main/java/io/cos/cas/osf/authentication/metadata/OsfPostgresAuthenticationMetaDataPopulator.java index 67d491d..de3b22f 100644 --- a/src/main/java/io/cos/cas/osf/authentication/metadata/OsfPostgresAuthenticationMetaDataPopulator.java +++ b/src/main/java/io/cos/cas/osf/authentication/metadata/OsfPostgresAuthenticationMetaDataPopulator.java @@ -27,17 +27,22 @@ public void populateAttributes(final AuthenticationBuilder builder, final Authen transaction.getPrimaryCredential().ifPresent(r -> { final OsfPostgresCredential credential = (OsfPostgresCredential) r; LOGGER.debug( - "Credential is of type [{}], thus adding attributes [{}, {}, {}, {}]", + "Credential is of type [{}], thus adding attributes [{}, {}, {}, {}, {}]", OsfPostgresCredential.class.getSimpleName(), OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_REMEMBER_ME, OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_REMOTE_PRINCIPAL, OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_DELEGATION_PROTOCOL, - OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_INSTITUTION_ID + OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_INSTITUTION_ID, + OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_TOS_CONSENT ); builder.addAttribute( OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_REMEMBER_ME, credential.isRememberMe() ); + builder.addAttribute( + OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_TOS_CONSENT, + credential.isTermsOfServiceChecked() + ); builder.addAttribute( OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_REMOTE_PRINCIPAL, credential.isRemotePrincipal() diff --git a/src/main/java/io/cos/cas/osf/authentication/support/DelegationProtocol.java b/src/main/java/io/cos/cas/osf/authentication/support/DelegationProtocol.java index fb352f2..750d58e 100644 --- a/src/main/java/io/cos/cas/osf/authentication/support/DelegationProtocol.java +++ b/src/main/java/io/cos/cas/osf/authentication/support/DelegationProtocol.java @@ -8,7 +8,7 @@ */ public enum DelegationProtocol { - NONE_OSF("none-osf"), + NONE("none"), OAUTH_PAC4J("oauth-pac4j"), diff --git a/src/main/java/io/cos/cas/osf/authentication/support/OsfInstitutionUtils.java b/src/main/java/io/cos/cas/osf/authentication/support/OsfInstitutionUtils.java new file mode 100644 index 0000000..5f7d615 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/authentication/support/OsfInstitutionUtils.java @@ -0,0 +1,75 @@ +package io.cos.cas.osf.authentication.support; + +import io.cos.cas.osf.dao.JpaOsfDao; +import io.cos.cas.osf.model.OsfInstitution; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * This is {@link OsfInstitutionUtils}. + * + * @author Longze Chen + * @since 21.0.0 + */ +@Slf4j +public final class OsfInstitutionUtils { + + public static boolean validateInstitutionForLogin(final JpaOsfDao jpaOsfDao, final String id) { + final OsfInstitution institution = jpaOsfDao.findOneInstitutionById(id); + return institution != null && institution.getDelegationProtocol() != null; + } + + public static Map getInstitutionLoginUrlMap( + final JpaOsfDao jpaOsfDao, + final String target, + final String id + ) { + List institutionList = new LinkedList<>(); + if (id == null || id.isEmpty()) { + institutionList = jpaOsfDao.findAllInstitutions(); + } else { + final OsfInstitution institution = jpaOsfDao.findOneInstitutionById(id); + if (institution != null) { + institutionList.add(institution); + } else { + institutionList = jpaOsfDao.findAllInstitutions(); + } + } + final Map institutionLoginUrlMap = new HashMap<>(); + for (final OsfInstitution institution: institutionList) { + final DelegationProtocol delegationProtocol = institution.getDelegationProtocol(); + if (DelegationProtocol.SAML_SHIB.equals(delegationProtocol)) { + institutionLoginUrlMap.put( + institution.getLoginUrl() + "&target=" + target + '#' + institution.getId(), + institution.getName() + ); + } else if (DelegationProtocol.CAS_PAC4J.equals(delegationProtocol)) { + institutionLoginUrlMap.put(institution.getInstitutionId(), institution.getName()); + } + } + return institutionLoginUrlMap; + } + + public static > Map sortByValue(final Map map) { + final List> list = new LinkedList<>(map.entrySet()); + Collections.sort(list, new Comparator>() { + @Override + public int compare(final Map.Entry e1, final Map.Entry e2) { + return (e1.getValue()).compareTo(e2.getValue()); + } + }); + final Map result = new LinkedHashMap<>(); + for (final Map.Entry entry : list) { + result.put(entry.getKey(), entry.getValue()); + } + return result; + } +} diff --git a/src/main/java/io/cos/cas/osf/configuration/model/OsfApiProperties.java b/src/main/java/io/cos/cas/osf/configuration/model/OsfApiProperties.java new file mode 100644 index 0000000..51970e0 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/configuration/model/OsfApiProperties.java @@ -0,0 +1,41 @@ +package io.cos.cas.osf.configuration.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * This is {@link OsfApiProperties}. + * + * @author Longze Chen + * @since 21.0.0 + */ +@Getter +@Setter +@Accessors(chain = true) +public class OsfApiProperties implements Serializable { + + private static final long serialVersionUID = 427830234394415772L; + + /** + * The institution authentication endpoint of OSF API. + */ + private String instnAuthnEndpoint; + + /** + * The secret that is used for signing the JWT claim. + */ + private String instnAuthnJwtSecret; + + /** + * The secret that is used for encrpyting the signed JWT claim. + */ + private String instnAuthnJweSecret; + + /** + * The location of the XSL file that is used for transforming XML authentication responses. + */ + private String instnAuthnXslLocation; +} diff --git a/src/main/java/io/cos/cas/osf/configuration/model/OsfUrlProperties.java b/src/main/java/io/cos/cas/osf/configuration/model/OsfUrlProperties.java new file mode 100644 index 0000000..be50615 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/configuration/model/OsfUrlProperties.java @@ -0,0 +1,80 @@ +package io.cos.cas.osf.configuration.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * This is {@link OsfUrlProperties}. + * + * CASv4.1.x-based oldCAS uses Java Server Pages (JSP) as the template engine with Spring Framework. Settings defined + * in .properties files can be accessed directly in JSP. However, this is no longer the case for CASv6.2.x-based newCAS + * which uses Thymeleaf as the template engine with Spring Framework. Thus, server-specific (i.e. production, test and + * staging servers) URLs need to be manually put into the flow context for the templates to access. + * + * @author Longze Chen + * @since 21.0.0 + */ +@Getter +@Setter +@Accessors(chain = true) +public class OsfUrlProperties implements Serializable { + + private static final long serialVersionUID = 5799818150523709901L; + + /** + * OSF home page URL. + */ + private String home; + + /** + * OSF dashboard page URL (must-be-signed-in). + */ + private String dashboard; + + /** + * OSF sign-up page URL. + */ + private String register; + + /** + * OSF login endpoint with "?next=". + */ + private String loginWithNext; + + /** + * OSF logout endpoint URL. + */ + private String logout; + + /** + * OSF resend-confirmation page URL. + */ + private String resendConfirmation; + + /** + * OSF forgot-password page URL. + */ + private String forgotPassword; + + /** + * OSF forgot-password-institutions page URL. + */ + private String forgotPasswordInstitution; + + /** + * OSF institutions home page URL. + */ + private String institutionsHome; + + /** + * Build the default service URL using OSF login endpoint with OSF home page as destination. + */ + public String constructDefaultServiceUrl() { + return loginWithNext + URLEncoder.encode(home, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/io/cos/cas/osf/dao/AbstractOsfDao.java b/src/main/java/io/cos/cas/osf/dao/AbstractOsfDao.java index e95f9ef..555e46c 100644 --- a/src/main/java/io/cos/cas/osf/dao/AbstractOsfDao.java +++ b/src/main/java/io/cos/cas/osf/dao/AbstractOsfDao.java @@ -2,9 +2,12 @@ import io.cos.cas.osf.model.OsfEmail; import io.cos.cas.osf.model.OsfGuid; +import io.cos.cas.osf.model.OsfInstitution; import io.cos.cas.osf.model.OsfTotp; import io.cos.cas.osf.model.OsfUser; +import java.util.List; + /** * This is {@link AbstractOsfDao}. * @@ -31,4 +34,8 @@ public OsfUser findOneUserByEmail(final String address) { protected abstract OsfEmail findOneEmailByAddress(String emailAddress); protected abstract OsfTotp findOneTotpByOwnerId(final Integer ownerId); + + protected abstract OsfInstitution findOneInstitutionById(final String id); + + protected abstract List findAllInstitutions(); } diff --git a/src/main/java/io/cos/cas/osf/dao/JpaOsfDao.java b/src/main/java/io/cos/cas/osf/dao/JpaOsfDao.java index 7737594..0b4aa4f 100644 --- a/src/main/java/io/cos/cas/osf/dao/JpaOsfDao.java +++ b/src/main/java/io/cos/cas/osf/dao/JpaOsfDao.java @@ -2,6 +2,7 @@ import io.cos.cas.osf.model.OsfEmail; import io.cos.cas.osf.model.OsfGuid; +import io.cos.cas.osf.model.OsfInstitution; import io.cos.cas.osf.model.OsfTotp; import io.cos.cas.osf.model.OsfUser; @@ -14,6 +15,7 @@ import javax.persistence.PersistenceException; import javax.persistence.TypedQuery; import javax.validation.constraints.NotNull; +import java.util.List; /** * This is {@link JpaOsfDao}. @@ -89,4 +91,33 @@ public OsfTotp findOneTotpByOwnerId(final Integer ownerId) { return null; } } + + @Override + public OsfInstitution findOneInstitutionById(final String id) { + try { + final TypedQuery query = entityManager.createQuery( + "select i from OsfInstitution i where i.institutionId = :id", + OsfInstitution.class + ); + query.setParameter("id", id); + return query.getSingleResult(); + } catch (final PersistenceException e) { + return null; + } + + } + + @Override + public List findAllInstitutions() { + try { + final TypedQuery query = entityManager.createQuery( + "select i from OsfInstitution i " + + "where (not i.delegationProtocol = '') and i.deleted = false", + OsfInstitution.class + ); + return query.getResultList(); + } catch (final PersistenceException e) { + return null; + } + } } diff --git a/src/main/java/io/cos/cas/osf/model/OsfInstitution.java b/src/main/java/io/cos/cas/osf/model/OsfInstitution.java new file mode 100644 index 0000000..9c80ea4 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/model/OsfInstitution.java @@ -0,0 +1,53 @@ +package io.cos.cas.osf.model; + +import io.cos.cas.osf.authentication.support.DelegationProtocol; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * This is {@link OsfInstitution}. + * + * @author Longze Chen + * @since 21.0.0 + */ +@Entity +@Table(name = "osf_institution") +@NoArgsConstructor +@Getter +@ToString +public class OsfInstitution extends AbstractOsfModel { + + private static final long serialVersionUID = -1186000864052788250L; + + @Column(name = "_id") + private String institutionId; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "login_url") + private String loginUrl; + + @Column(name = "logout_url") + private String logoutUrl; + + @Column(name = "delegation_protocol") + private String delegationProtocol; + + @Column(name = "is_deleted") + private Boolean deleted; + + public DelegationProtocol getDelegationProtocol() { + try { + return DelegationProtocol.getType(delegationProtocol); + } catch (final IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/io/cos/cas/osf/model/OsfUser.java b/src/main/java/io/cos/cas/osf/model/OsfUser.java index 6249bf2..4a7e574 100644 --- a/src/main/java/io/cos/cas/osf/model/OsfUser.java +++ b/src/main/java/io/cos/cas/osf/model/OsfUser.java @@ -62,6 +62,10 @@ public final class OsfUser extends AbstractOsfModel { @Column(name = "date_confirmed") private Date dateConfirmed; + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "accepted_terms_of_service") + private Date dateTermsOfServiceAccepted; + @Temporal(TemporalType.TIMESTAMP) @Column(name = "date_disabled") private Date dateDisabled; @@ -86,6 +90,10 @@ public boolean isConfirmed() { return dateConfirmed != null; } + public boolean isTermsOfServiceAccepted() { + return dateTermsOfServiceAccepted != null; + } + public boolean isDisabled() { return dateDisabled != null; } diff --git a/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java b/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java index da287d8..95df58b 100644 --- a/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java +++ b/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java @@ -1,7 +1,9 @@ package io.cos.cas.osf.web.config; +import io.cos.cas.osf.dao.JpaOsfDao; import io.cos.cas.osf.web.flow.login.OsfDefaultLoginPreparationAction; import io.cos.cas.osf.web.flow.login.OsfInstitutionLoginPreparationAction; +import io.cos.cas.osf.web.flow.login.OsfCasPreInitialFlowSetupAction; import io.cos.cas.osf.web.flow.login.OsfPrincipalFromNonInteractiveCredentialsAction; import org.apereo.cas.CentralAuthenticationService; @@ -46,7 +48,6 @@ public class OsfCasSupportActionsConfiguration extends CasSupportActionsConfigur @Qualifier("initialAuthenticationAttemptWebflowEventResolver") private ObjectProvider initialAuthenticationAttemptWebflowEventResolver; - @Autowired @Qualifier("adaptiveAuthenticationPolicy") private ObjectProvider adaptiveAuthenticationPolicy; @@ -55,6 +56,19 @@ public class OsfCasSupportActionsConfiguration extends CasSupportActionsConfigur @Qualifier("centralAuthenticationService") private ObjectProvider centralAuthenticationService; + @Autowired + private ObjectProvider jpaOsfDao; + + /** + * Bean configuration for {@link OsfCasPreInitialFlowSetupAction}. + * + * @return the initialized action + */ + @Bean + public Action osfCasPreInitialFlowSetupAction() { + return new OsfCasPreInitialFlowSetupAction(casProperties.getAuthn().getOsfUrl()); + } + /** * Bean configuration for {@link OsfPrincipalFromNonInteractiveCredentialsAction}. * @@ -76,6 +90,8 @@ public Action osfNonInteractiveAuthenticationCheckAction() { serviceTicketRequestWebflowEventResolver.getObject(), adaptiveAuthenticationPolicy.getObject(), centralAuthenticationService.getObject(), + casProperties.getAuthn().getOsfUrl(), + casProperties.getAuthn().getOsfApi(), authnDelegationClients ); } @@ -104,7 +120,9 @@ public Action osfInstitutionLoginCheckAction() { return new OsfInstitutionLoginPreparationAction( initialAuthenticationAttemptWebflowEventResolver.getObject(), serviceTicketRequestWebflowEventResolver.getObject(), - adaptiveAuthenticationPolicy.getObject() + adaptiveAuthenticationPolicy.getObject(), + jpaOsfDao.getObject(), + casProperties.getAuthn().getOsfPostgres().getInstitutionClients() ); } } diff --git a/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasCoreWebflowConfiguration.java b/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasCoreWebflowConfiguration.java index 076b5a8..a1bc0bd 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasCoreWebflowConfiguration.java +++ b/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasCoreWebflowConfiguration.java @@ -2,12 +2,13 @@ import io.cos.cas.osf.authentication.exception.AccountNotConfirmedIdpException; import io.cos.cas.osf.authentication.exception.AccountNotConfirmedOsfException; -import io.cos.cas.osf.authentication.exception.InstitutionSsoNotImplementedException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoFailedException; import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException; import io.cos.cas.osf.authentication.exception.InvalidPasswordException; import io.cos.cas.osf.authentication.exception.InvalidUserStatusException; import io.cos.cas.osf.authentication.exception.InvalidVerificationKeyException; import io.cos.cas.osf.authentication.exception.OneTimePasswordRequiredException; +import io.cos.cas.osf.authentication.exception.TermsOfServiceConsentRequiredException; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.web.flow.config.CasCoreWebflowConfiguration; @@ -42,11 +43,12 @@ public Set> handledAuthenticationExceptions() { errors.add(AccountNotConfirmedIdpException.class); errors.add(AccountNotConfirmedOsfException.class); errors.add(InvalidOneTimePasswordException.class); - errors.add(InstitutionSsoNotImplementedException.class); + errors.add(InstitutionSsoFailedException.class); errors.add(InvalidPasswordException.class); errors.add(InvalidUserStatusException.class); errors.add(InvalidVerificationKeyException.class); errors.add(OneTimePasswordRequiredException.class); + errors.add(TermsOfServiceConsentRequiredException.class); // Add built-in exceptions after OSF-specific exceptions since order matters errors.addAll(super.handledAuthenticationExceptions()); diff --git a/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasWebflowContextConfiguration.java b/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasWebflowContextConfiguration.java index bd10bc3..64ac4ac 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasWebflowContextConfiguration.java +++ b/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasWebflowContextConfiguration.java @@ -1,6 +1,7 @@ package io.cos.cas.osf.web.flow.config; import io.cos.cas.osf.web.flow.configurer.OsfCasLoginWebflowConfigurer; +import io.cos.cas.osf.web.flow.configurer.OsfCasLogoutWebFlowConfigurer; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.web.flow.CasWebflowConfigurer; @@ -46,4 +47,16 @@ public CasWebflowConfigurer defaultWebflowConfigurer() { osfCasLoginWebflowConfigurer.setOrder(Ordered.HIGHEST_PRECEDENCE); return osfCasLoginWebflowConfigurer; } + + @Override + @Bean + @Order(DEFAULT_WEB_FLOW_CONFIGURER_ORDER) + @Lazy(false) + public CasWebflowConfigurer defaultLogoutWebflowConfigurer() { + OsfCasLogoutWebFlowConfigurer osfCasLogoutWebFlowConfigurer + = new OsfCasLogoutWebFlowConfigurer(builder(), loginFlowRegistry(), applicationContext, casProperties); + osfCasLogoutWebFlowConfigurer.setLogoutFlowDefinitionRegistry(logoutFlowRegistry()); + osfCasLogoutWebFlowConfigurer.setOrder(Ordered.HIGHEST_PRECEDENCE); + return osfCasLogoutWebFlowConfigurer; + } } diff --git a/src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLoginWebflowConfigurer.java b/src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLoginWebflowConfigurer.java index bb9f9e8..87b7e41 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLoginWebflowConfigurer.java +++ b/src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLoginWebflowConfigurer.java @@ -3,11 +3,12 @@ import io.cos.cas.osf.authentication.credential.OsfPostgresCredential; import io.cos.cas.osf.authentication.exception.AccountNotConfirmedIdpException; import io.cos.cas.osf.authentication.exception.AccountNotConfirmedOsfException; -import io.cos.cas.osf.authentication.exception.InstitutionSsoNotImplementedException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoFailedException; import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException; import io.cos.cas.osf.authentication.exception.InvalidUserStatusException; import io.cos.cas.osf.authentication.exception.InvalidVerificationKeyException; import io.cos.cas.osf.authentication.exception.OneTimePasswordRequiredException; +import io.cos.cas.osf.authentication.exception.TermsOfServiceConsentRequiredException; import io.cos.cas.osf.web.flow.support.OsfCasWebflowConstants; import org.apereo.cas.authentication.PrincipalException; @@ -26,6 +27,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.webflow.core.collection.MutableAttributeMap; import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.engine.ActionList; import org.springframework.webflow.engine.ActionState; import org.springframework.webflow.engine.Flow; import org.springframework.webflow.engine.History; @@ -66,12 +68,23 @@ public OsfCasLoginWebflowConfigurer( super(flowBuilderServices, flowDefinitionRegistry, applicationContext, casProperties); } + @Override + protected void createInitialFlowActions(final Flow flow) { + final ActionList startActionList = flow.getStartActionList(); + startActionList.add(createEvaluateAction(OsfCasWebflowConstants.ACTION_ID_OSF_PRE_INITIAL_FLOW_SETUP)); + super.createInitialFlowActions(flow); + } + @Override protected void createDefaultViewStates(final Flow flow) { super.createDefaultViewStates(flow); // Create OSF customized view states createTwoFactorLoginFormView(flow); + createTermsOfServiceConsentLoginFormView(flow); + createInstitutionLoginView(flow); + createUnsupportedInstitutionLoginView(flow); createOrcidLoginAutoRedirectView(flow); + createDefaultServiceLoginAutoRedirectView(flow); createOsfCasAuthenticationExceptionViewStates(flow); } @@ -238,8 +251,13 @@ protected void createHandleAuthenticationFailureAction(final Flow flow) { ); createTransitionForState( handler, - InstitutionSsoNotImplementedException.class.getSimpleName(), - OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_NOT_IMPLEMENTED + TermsOfServiceConsentRequiredException.class.getSimpleName(), + OsfCasWebflowConstants.VIEW_ID_TERMS_OF_SERVICE_CONSENT_REQUIRED + ); + createTransitionForState( + handler, + InstitutionSsoFailedException.class.getSimpleName(), + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_FAILED ); // The default transition @@ -310,11 +328,21 @@ private void createOsfDefaultLoginCheckAction(final Flow flow) { OsfCasWebflowConstants.TRANSITION_ID_INSTITUTION_LOGIN, OsfCasWebflowConstants.STATE_ID_OSF_INSTITUTION_LOGIN_CHECK ); + createTransitionForState( + action, + OsfCasWebflowConstants.TRANSITION_ID_UNSUPPORTED_INSTITUTION_LOGIN, + OsfCasWebflowConstants.VIEW_ID_UNSUPPORTED_INSTITUTION_SSO_INIT + ); createTransitionForState( action, OsfCasWebflowConstants.TRANSITION_ID_ORCID_LOGIN_AUTO_REDIRECT, OsfCasWebflowConstants.VIEW_ID_ORCID_LOGIN_AUTO_REDIRECT ); + createTransitionForState( + action, + OsfCasWebflowConstants.TRANSITION_ID_DEFAULT_SERVICE_LOGIN_AUTO_REDIRECT, + OsfCasWebflowConstants.VIEW_ID_DEFAULT_SERVICE_LOGIN_AUTO_REDIRECT + ); createTransitionForState( action, CasWebflowConstants.TRANSITION_ID_ERROR, @@ -336,7 +364,12 @@ private void createOsfInstitutionLoginCheckAction(final Flow flow) { createTransitionForState( action, CasWebflowConstants.TRANSITION_ID_ERROR, - OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_NOT_IMPLEMENTED + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_FAILED + ); + createTransitionForState( + action, + CasWebflowConstants.TRANSITION_ID_SUCCESS, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_INIT ); } @@ -368,8 +401,8 @@ private void createOsfCasAuthenticationExceptionViewStates(final Flow flow) { ); createViewState( flow, - OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_NOT_IMPLEMENTED, - OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_NOT_IMPLEMENTED + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_FAILED, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_FAILED ); } @@ -407,6 +440,40 @@ private void createTwoFactorLoginFormView(final Flow flow) { attributes.put("history", History.INVALIDATE); } + /** + * Create the customized terms of service consent form submission view state for OSF CAS. + * + * @param flow the flow + */ + private void createTermsOfServiceConsentLoginFormView(final Flow flow) { + List propertiesToBind = CollectionUtils.wrapList("termsOfServiceChecked", "source"); + BinderConfiguration binder = createStateBinderConfiguration(propertiesToBind); + casProperties.getView().getCustomLoginFormFields() + .forEach((field, props) -> { + String fieldName = String.format("customFields[%s]", field); + binder.addBinding( + new BinderConfiguration.Binding(fieldName, props.getConverter(), props.isRequired()) + ); + }); + ViewState state = createViewState( + flow, + OsfCasWebflowConstants.VIEW_ID_TERMS_OF_SERVICE_CONSENT_REQUIRED, + OsfCasWebflowConstants.VIEW_ID_TERMS_OF_SERVICE_CONSENT_REQUIRED, + binder + ); + state.getRenderActionList().add(createEvaluateAction(CasWebflowConstants.ACTION_ID_RENDER_LOGIN_FORM)); + createStateModelBinding(state, CasWebflowConstants.VAR_ID_CREDENTIAL, OsfPostgresCredential.class); + Transition transition = createTransitionForState( + state, + CasWebflowConstants.TRANSITION_ID_SUBMIT, + CasWebflowConstants.STATE_ID_REAL_SUBMIT + ); + MutableAttributeMap attributes = transition.getAttributes(); + attributes.put("bind", Boolean.TRUE); + attributes.put("validate", Boolean.TRUE); + attributes.put("history", History.INVALIDATE); + } + /** * Create the ORCiD login auto-redirect view to support the OSF feature "sign-up via ORCiD". * @@ -419,4 +486,43 @@ protected void createOrcidLoginAutoRedirectView(final Flow flow) { OsfCasWebflowConstants.VIEW_ID_ORCID_LOGIN_AUTO_REDIRECT ); } + + /** + * Create the ORCiD login auto-redirect view to support the OSF feature "sign-up via ORCiD". + * + * @param flow the flow + */ + protected void createDefaultServiceLoginAutoRedirectView(final Flow flow) { + createViewState( + flow, + OsfCasWebflowConstants.VIEW_ID_DEFAULT_SERVICE_LOGIN_AUTO_REDIRECT, + OsfCasWebflowConstants.VIEW_ID_DEFAULT_SERVICE_LOGIN_AUTO_REDIRECT + ); + } + + /** + * Create the institution SSO init view state to support the OSF feature "sign-in via institutions". + * + * @param flow the flow + */ + protected void createInstitutionLoginView(final Flow flow) { + createViewState( + flow, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_INIT, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_INIT + ); + } + + /** + * Create the unsupported institution view state to support the OSF feature "I can't find my institution". + * + * @param flow the flow + */ + protected void createUnsupportedInstitutionLoginView(final Flow flow) { + createViewState( + flow, + OsfCasWebflowConstants.VIEW_ID_UNSUPPORTED_INSTITUTION_SSO_INIT, + OsfCasWebflowConstants.VIEW_ID_UNSUPPORTED_INSTITUTION_SSO_INIT + ); + } } diff --git a/src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLogoutWebFlowConfigurer.java b/src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLogoutWebFlowConfigurer.java new file mode 100644 index 0000000..97b4e65 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLogoutWebFlowConfigurer.java @@ -0,0 +1,49 @@ +package io.cos.cas.osf.web.flow.configurer; + +import io.cos.cas.osf.web.flow.support.OsfCasWebflowConstants; + +import org.apereo.cas.configuration.CasConfigurationProperties; +import org.apereo.cas.web.flow.configurer.DefaultLogoutWebflowConfigurer; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.engine.ActionList; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.builder.support.FlowBuilderServices; + +/** + * This is {@link OsfCasLogoutWebFlowConfigurer}. + * + * @author Longze Chen + * @since 21.0.0 + */ +public class OsfCasLogoutWebFlowConfigurer extends DefaultLogoutWebflowConfigurer { + + public OsfCasLogoutWebFlowConfigurer( + final FlowBuilderServices flowBuilderServices, + final FlowDefinitionRegistry flowDefinitionRegistry, + final ConfigurableApplicationContext applicationContext, + final CasConfigurationProperties casProperties + ) { + super(flowBuilderServices, flowDefinitionRegistry, applicationContext, casProperties); + } + + @Override + protected void doInitialize() { + final Flow flow = getLogoutFlow(); + if (flow != null) { + createInitialFlowActions(flow); + } + super.doInitialize(); + } + + /** + * Create initial flow actions similar to {@link OsfCasLoginWebflowConfigurer#createInitialFlowActions(Flow)}. + * + * @param flow the flow + */ + protected void createInitialFlowActions(final Flow flow) { + final ActionList startActionList = flow.getStartActionList(); + startActionList.add(createEvaluateAction(OsfCasWebflowConstants.ACTION_ID_OSF_PRE_INITIAL_FLOW_SETUP)); + } +} diff --git a/src/main/java/io/cos/cas/osf/web/flow/login/OsfAbstractLoginPreparationAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfAbstractLoginPreparationAction.java index d68742d..9751e49 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/login/OsfAbstractLoginPreparationAction.java +++ b/src/main/java/io/cos/cas/osf/web/flow/login/OsfAbstractLoginPreparationAction.java @@ -8,6 +8,10 @@ import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + /** * This is {@link OsfAbstractLoginPreparationAction}. * @@ -17,7 +21,7 @@ * are NOOP. * * @author Longze Chen - * @since 20.0.0 + * @since 20.1.0 */ public abstract class OsfAbstractLoginPreparationAction extends AbstractAuthenticationAction { @@ -29,14 +33,22 @@ public abstract class OsfAbstractLoginPreparationAction extends AbstractAuthenti protected static final String PARAMETER_CAMPAIGN_VALUE = "institution"; + protected static final String PARAMETER_CAMPAIGN_UNSUPPORTED_INSTITUTION_VALUE = "unsupportedinstitution"; + protected static final String PARAMETER_INSTITUTION_ID = "institutionId"; protected static final String PARAMETER_ORCID_CLIENT_TYPE = "orcid"; + protected static final String PARAMETER_CAS_CLIENT_TYPE = "cas"; + protected static final String PARAMETER_ORCID_REDIRECT = "redirectOrcid"; protected static final String PARAMETER_ORCID_REDIRECT_VALUE = "true"; + protected static final String PARAMETER_REDIRECT_SOURCE = "casRedirectSource"; + + protected static final List EXPECTED_REDIRECT_CODES = new LinkedList<>(Arrays.asList("tomcat", "cas")); + public OsfAbstractLoginPreparationAction( final CasDelegatingWebflowEventResolver initialAuthenticationAttemptWebflowEventResolver, final CasWebflowEventResolver serviceTicketRequestWebflowEventResolver, diff --git a/src/main/java/io/cos/cas/osf/web/flow/login/OsfCasPreInitialFlowSetupAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfCasPreInitialFlowSetupAction.java new file mode 100644 index 0000000..4b6a18c --- /dev/null +++ b/src/main/java/io/cos/cas/osf/web/flow/login/OsfCasPreInitialFlowSetupAction.java @@ -0,0 +1,39 @@ +package io.cos.cas.osf.web.flow.login; + +import io.cos.cas.osf.configuration.model.OsfUrlProperties; + +import io.cos.cas.osf.web.flow.support.OsfCasWebflowConstants; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.webflow.action.AbstractAction; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +import javax.validation.constraints.NotNull; +import java.util.Optional; + +/** + * This is {@link OsfCasPreInitialFlowSetupAction}. + * + * @author Longze Chen + * @since 21.0.0 + */ +@RequiredArgsConstructor +@Slf4j +public class OsfCasPreInitialFlowSetupAction extends AbstractAction { + + @NotNull + private final OsfUrlProperties osfUrlProperties; + + @Override + protected Event doExecute(final RequestContext context) { + final OsfUrlProperties osfUrl = Optional.of(context).map( + requestContext -> (OsfUrlProperties) requestContext.getFlowScope().get(OsfCasWebflowConstants.FLOW_PARAMETER_OSF_URL) + ).orElse(null); + if (osfUrl == null) { + context.getFlowScope().put(OsfCasWebflowConstants.FLOW_PARAMETER_OSF_URL, osfUrlProperties); + } + return success(); + } +} diff --git a/src/main/java/io/cos/cas/osf/web/flow/login/OsfDefaultLoginPreparationAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfDefaultLoginPreparationAction.java index 157bc5f..1c94d41 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/login/OsfDefaultLoginPreparationAction.java +++ b/src/main/java/io/cos/cas/osf/web/flow/login/OsfDefaultLoginPreparationAction.java @@ -1,5 +1,6 @@ package io.cos.cas.osf.web.flow.login; +import io.cos.cas.osf.configuration.model.OsfUrlProperties; import io.cos.cas.osf.web.flow.support.OsfCasWebflowConstants; import io.cos.cas.osf.web.support.OsfCasLoginContext; @@ -29,7 +30,7 @@ * pass, current and potential future states; and finally 3) return respective transition events to the login web flow. * * @author Longze Chen - * @since 20.0.0 + * @since 20.1.0 */ @Slf4j public class OsfDefaultLoginPreparationAction extends OsfAbstractLoginPreparationAction { @@ -49,61 +50,89 @@ public OsfDefaultLoginPreparationAction( @Override protected Event doExecute(RequestContext context) { - OsfCasLoginContext loginContext; - final String serviceUrl = getEncodedServiceUrlFromRequestContext(context); final boolean institutionLogin = isInstitutionLogin(context); final String institutionId = getInstitutionIdFromRequestContext(context); + final boolean unsupportedInstitutionLogin = isUnsupportedInstitutionLogin(context); final boolean orcidRedirect = isOrcidLoginAutoRedirect(context); final String orcidLoginUrl = getOrcidLoginUrlFromFlowScope(context); + final String encodedServiceUrl = getEncodedServiceUrlFromRequestContext(context); + final boolean defaultService = isFromFlowlessErrorPage(context); + final OsfUrlProperties osfUrl = Optional.of(context).map( + requestContext -> (OsfUrlProperties) requestContext.getFlowScope().get(OsfCasWebflowConstants.FLOW_PARAMETER_OSF_URL) + ).orElse(null); + if (osfUrl == null) { + LOGGER.error("The login web flow has not been initialized correctly."); + return error(); + } + final String defaultServiceUrl = osfUrl.constructDefaultServiceUrl(); + + OsfCasLoginContext loginContext; loginContext = Optional.of(context).map(requestContext -> (OsfCasLoginContext) requestContext.getFlowScope().get(PARAMETER_LOGIN_CONTEXT)).orElse(null); if (loginContext == null) { loginContext = new OsfCasLoginContext( - serviceUrl, + encodedServiceUrl, institutionLogin, institutionId, + unsupportedInstitutionLogin, orcidRedirect, - orcidLoginUrl + orcidLoginUrl, + defaultService, + defaultServiceUrl ); } else { - loginContext.setServiceUrl(serviceUrl); + loginContext.setEncodedServiceUrl(encodedServiceUrl); loginContext.setInstitutionLogin(institutionLogin); loginContext.setInstitutionId(institutionId); + loginContext.setUnsupportedInstitutionLogin(unsupportedInstitutionLogin); loginContext.setOrcidLoginUrl(orcidLoginUrl); loginContext.setOrcidRedirect(false); + loginContext.setDefaultService(defaultService); + loginContext.setDefaultServiceUrl(defaultServiceUrl); } context.getFlowScope().put(PARAMETER_LOGIN_CONTEXT, loginContext); - if (loginContext.isInstitutionLogin()) { + if (loginContext.isDefaultService()) { + if (StringUtils.isNotBlank(defaultServiceUrl)) { + return autoRedirectToDefaultServiceLogin(); + } + LOGGER.error("Default service login auto-redirect failed due to URL configurations not found in context."); + return error(); + } else if (loginContext.isInstitutionLogin()) { return switchToInstitutionLogin(); - } - - if (loginContext.isOrcidRedirect()) { + } else if (loginContext.isUnsupportedInstitutionLogin()) { + return switchToUnsupportedInstitutionLogin(); + } else if (loginContext.isOrcidRedirect()) { if (StringUtils.isNotBlank(orcidLoginUrl)) { return autoRedirectToOrcidLogin(); } LOGGER.error("ORCiD login auto-redirect failed due to delegation configurations not found in context."); return error(); + } else { + return continueToUsernamePasswordLogin(); } - - return continueToUsernamePasswordLogin(); } private boolean isInstitutionLogin(final RequestContext context) { final String campaign = context.getRequestParameters().get(PARAMETER_CAMPAIGN); - return StringUtils.isNotBlank(campaign) && PARAMETER_CAMPAIGN_VALUE.equals(campaign.toLowerCase()); + return StringUtils.isNotBlank(campaign) && PARAMETER_CAMPAIGN_VALUE.equalsIgnoreCase(campaign); } private String getInstitutionIdFromRequestContext(final RequestContext context) { final String institutionId = context.getRequestParameters().get(PARAMETER_INSTITUTION_ID); - return StringUtils.isNotBlank(institutionId) ? null : institutionId; + return StringUtils.isNotBlank(institutionId) ? institutionId : null; + } + + private boolean isUnsupportedInstitutionLogin(final RequestContext context) { + final String campaign = context.getRequestParameters().get(PARAMETER_CAMPAIGN); + return StringUtils.isNotBlank(campaign) && PARAMETER_CAMPAIGN_UNSUPPORTED_INSTITUTION_VALUE.equalsIgnoreCase(campaign); } private boolean isOrcidLoginAutoRedirect(final RequestContext context) { final String orcidRedirect = context.getRequestParameters().get(PARAMETER_ORCID_REDIRECT); return StringUtils.isNotBlank(orcidRedirect) - && PARAMETER_ORCID_REDIRECT_VALUE.equals(orcidRedirect.toLowerCase()); + && PARAMETER_ORCID_REDIRECT_VALUE.equalsIgnoreCase(orcidRedirect); } private String getOrcidLoginUrlFromFlowScope(final RequestContext context) { @@ -127,6 +156,11 @@ private String getEncodedServiceUrlFromRequestContext(final RequestContext conte return URLEncoder.encode(serviceUrl, StandardCharsets.UTF_8); } + private boolean isFromFlowlessErrorPage(final RequestContext context) { + final String errorCode = context.getRequestParameters().get(PARAMETER_REDIRECT_SOURCE); + return !StringUtils.isBlank(errorCode) && EXPECTED_REDIRECT_CODES.contains(errorCode); + } + private Event continueToUsernamePasswordLogin() { return new Event(this, OsfCasWebflowConstants.TRANSITION_ID_USERNAME_PASSWORD_LOGIN); } @@ -135,7 +169,15 @@ private Event switchToInstitutionLogin() { return new Event(this, OsfCasWebflowConstants.TRANSITION_ID_INSTITUTION_LOGIN); } + private Event switchToUnsupportedInstitutionLogin() { + return new Event(this, OsfCasWebflowConstants.TRANSITION_ID_UNSUPPORTED_INSTITUTION_LOGIN); + } + private Event autoRedirectToOrcidLogin() { return new Event(this, OsfCasWebflowConstants.TRANSITION_ID_ORCID_LOGIN_AUTO_REDIRECT); } + + private Event autoRedirectToDefaultServiceLogin() { + return new Event(this, OsfCasWebflowConstants.TRANSITION_ID_DEFAULT_SERVICE_LOGIN_AUTO_REDIRECT); + } } diff --git a/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java index f068f57..97e179c 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java +++ b/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java @@ -1,36 +1,120 @@ package io.cos.cas.osf.web.flow.login; +import io.cos.cas.osf.authentication.support.OsfInstitutionUtils; +import io.cos.cas.osf.dao.JpaOsfDao; +import io.cos.cas.osf.web.support.OsfCasLoginContext; + +import lombok.extern.slf4j.Slf4j; + import org.apereo.cas.authentication.adaptive.AdaptiveAuthenticationPolicy; +import org.apereo.cas.web.DelegatedClientIdentityProviderConfiguration; import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver; import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver; +import org.apereo.cas.web.support.WebUtils; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + /** * This is {@link OsfInstitutionLoginPreparationAction}. * * Extends {@link OsfAbstractLoginPreparationAction} to prepare the institution login page. * * @author Longze Chen - * @since 20.0.0 + * @since 20.1.0 */ +@Slf4j public class OsfInstitutionLoginPreparationAction extends OsfAbstractLoginPreparationAction { + @NotNull + private final JpaOsfDao jpaOsfDao; + + @NotNull + private final List pac4jInstnClients; + public OsfInstitutionLoginPreparationAction( final CasDelegatingWebflowEventResolver initialAuthenticationAttemptWebflowEventResolver, final CasWebflowEventResolver serviceTicketRequestWebflowEventResolver, - final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy + final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy, + final JpaOsfDao jpaOsfDao, + final List pac4jInstnClients ) { super( initialAuthenticationAttemptWebflowEventResolver, serviceTicketRequestWebflowEventResolver, adaptiveAuthenticationPolicy ); + this.jpaOsfDao = jpaOsfDao; + this.pac4jInstnClients = pac4jInstnClients; } @Override protected Event doExecute(RequestContext context) { - return error(); + + String target; + String service = context.getRequestParameters().get("service"); + if (service != null) { + service = URLEncoder.encode(service, StandardCharsets.UTF_8); + target = URLEncoder.encode(String.format("/login?service=%s", service), StandardCharsets.UTF_8); + } else { + target = URLEncoder.encode("/login", StandardCharsets.UTF_8); + } + + String institutionId = null; + OsfCasLoginContext loginContext; + loginContext = Optional.of(context).map(requestContext + -> (OsfCasLoginContext) requestContext.getFlowScope().get(PARAMETER_LOGIN_CONTEXT)).orElse(null); + if (loginContext != null) { + institutionId = loginContext.getInstitutionId(); + if (!OsfInstitutionUtils.validateInstitutionForLogin(jpaOsfDao, institutionId)) { + loginContext.setInstitutionId(null); + context.getFlowScope().put(PARAMETER_LOGIN_CONTEXT, loginContext); + institutionId = null; + } + } + + final Map institutionLoginUrlMap + = OsfInstitutionUtils.getInstitutionLoginUrlMap(jpaOsfDao, target, institutionId); + final Map institutionLoginUrlMapSorted; + if (institutionId != null) { + institutionLoginUrlMapSorted = institutionLoginUrlMap; + } else { + institutionLoginUrlMap.put("", " -- select an institution -- "); + institutionLoginUrlMapSorted = OsfInstitutionUtils.sortByValue(institutionLoginUrlMap); + } + context.getFlowScope().put("institutions", institutionLoginUrlMapSorted); + putPac4jInstnLoginUrls(context); + return success(); + } + + private void putPac4jInstnLoginUrls(final RequestContext context) { + final Map pac4jInstitutionLoginUrlMap = new HashMap<>(); + final Set clients = WebUtils.getDelegatedAuthenticationProviderConfigurations(context); + for (final Serializable client: clients) { + if (client instanceof DelegatedClientIdentityProviderConfiguration) { + final String clientType = ((DelegatedClientIdentityProviderConfiguration) client).getType(); + if (PARAMETER_CAS_CLIENT_TYPE.equals(clientType)) { + final String clientName = ((DelegatedClientIdentityProviderConfiguration) client).getName(); + if (!pac4jInstnClients.contains(clientName)) { + LOGGER.error("Invalid PAC4J institution clients: [{}]", clientName); + } else { + final String clientUrl = ((DelegatedClientIdentityProviderConfiguration) client).getRedirectUrl(); + pac4jInstitutionLoginUrlMap.put(clientName, clientUrl); + LOGGER.warn("{}: {}", clientName, clientUrl); + } + } + } + } + context.getFlowScope().put("pac4jInstnLoginUrls", pac4jInstitutionLoginUrlMap); } } diff --git a/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java index 519b93a..e237b31 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java +++ b/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java @@ -1,7 +1,25 @@ package io.cos.cas.osf.web.flow.login; import io.cos.cas.osf.authentication.credential.OsfPostgresCredential; +import io.cos.cas.osf.authentication.exception.InstitutionSsoFailedException; import io.cos.cas.osf.authentication.support.DelegationProtocol; +import io.cos.cas.osf.configuration.model.OsfApiProperties; +import io.cos.cas.osf.configuration.model.OsfUrlProperties; +import io.cos.cas.osf.web.support.OsfApiInstitutionAuthenticationResult; + +import com.nimbusds.jose.crypto.DirectEncrypter; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWEHeader; +import com.nimbusds.jose.JWEObject; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.Payload; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; import lombok.Getter; import lombok.Setter; @@ -9,21 +27,57 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.fluent.Request; +import org.apache.http.entity.ContentType; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.message.BasicHeader; import org.apereo.cas.CentralAuthenticationService; +import org.apereo.cas.authentication.Authentication; +import org.apereo.cas.authentication.AuthenticationException; import org.apereo.cas.authentication.adaptive.AdaptiveAuthenticationPolicy; import org.apereo.cas.authentication.Credential; import org.apereo.cas.authentication.principal.ClientCredential; +import org.apereo.cas.authentication.principal.Principal; +import org.apereo.cas.web.flow.CasWebflowConstants; import org.apereo.cas.web.flow.actions.AbstractNonInteractiveCredentialsAction; import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver; import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver; import org.apereo.cas.web.support.WebUtils; +import org.json.JSONObject; +import org.json.XML; + +import org.springframework.util.ResourceUtils; +import org.springframework.webflow.action.AbstractAction; +import org.springframework.webflow.core.collection.LocalAttributeMap; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.security.auth.login.AccountException; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.NotNull; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Map; @@ -31,10 +85,36 @@ * This is {@link OsfPrincipalFromNonInteractiveCredentialsAction}. * * Extends {@link AbstractNonInteractiveCredentialsAction} to check if there is any non-interactive authentication - * available. In the case of authentication delegation, if credential with matching client found, simply use that - * credential and return the success event (i.e. to authenticate and create the ticket granting ticket). In the case - * of "username / verification key" login, if both found in requests parameters, create the OSF credential and return - * success. Otherwise, return the error event and go to the login page for default "username / password" login. + * available. There are five different cases depends on whether there is an active client credential, whether there + * is a valid shibboleth authentication session, and whether there is a pair of username and verification key in + * request parameters. Credentials are checked first, then the shibboleth session, and finally the verification key. + * + * 1) In the case of non-institution pac4j authentication delegation (e.g. ORCiD), if credential with a matching + * client is found, simply use that credential and return the {@link AbstractAction#success()} event. + * + * For 1) only, the success event will trigger authentication with pac4j's authentication handler. + * + * 2) In the case of institution pac4j authentication delegation (e.g. OKState and Concordia), if credential with a + * matching client is found, extract the client info, principal ID and authentication attributes and store them into + * the {@code OsfPostgresCredential#delegationAttributes} object. + * + * 3) In the case of institution shibboleth authentication delegation (e.g. the rest of the institutions), if a valid + * shibboleth session (by checking the {@code AUTH-Shib-Session-Id} header) head is found, extract all AUTH headers + * and store them into the {@code OsfPostgresCredential#delegationAttributes} object. + * + * For both 2) and 3), OSF CAS then applies an XLS transformation to validate and convert delegation attributes into + * a JSON object using the "instn-authn.xsl" file. JWT sign andJWE encrypt the JSON object, and use it as the payload + * to authenticate against the OSF API institution authentication endpoint. If successful, update the credential and + * return the {@link AbstractAction#success()} event. + * + * 4) In the case of username and verification key login, if both found in requests parameters, store them into the + * {@link OsfPostgresCredential} and return the {@link AbstractAction#success()} event. + * + * For 2), 3) and 4), the success event will trigger authentication using the {@code OsfPostgresAuthenticationHandler}. + * + * 5) If none of the credential, shibboleth session or the pair of username and verification key are found, return the + * {@link AbstractAction#error()} event so that the login web flow will go to the pre-login check state and prepare for + * the username / password login form, ORCiD login button and institution login page. * * @author Longze Chen * @since 20.0.0 @@ -48,22 +128,45 @@ public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNon private static final String VERIFICATION_KEY_PARAMETER_NAME = "verification_key"; + private static final String OSF_URL_FLOW_PARAMETER = "osfUrl"; + private static final String AUTHENTICATION_EXCEPTION = "authnError"; + private static final int SIXTY_SECONDS = 60 * 1000; + public static final String INSTITUTION_CLIENTS_PARAMETER_NAME = "institutionClients"; public static final String NON_INSTITUTION_CLIENTS_PARAMETER_NAME = "nonInstitutionClients"; + private static final String REMOTE_USER = "remote_user"; + + private static final String ATTRIBUTE_PREFIX = "auth-"; + + private static final String SHIBBOLETH_SESSION_HEADER = ATTRIBUTE_PREFIX + "shib-session-id"; + + private static final String SHIBBOLETH_COOKIE_PREFIX = "_shibsession_"; + @NotNull private CentralAuthenticationService centralAuthenticationService; + @NotNull + private OsfUrlProperties osfUrlProperties; + + @NotNull + private OsfApiProperties osfApiProperties; + + @NotNull private Map> authnDelegationClients; + private Transformer instnAuthnRespTransformer; + public OsfPrincipalFromNonInteractiveCredentialsAction( final CasDelegatingWebflowEventResolver initialAuthenticationAttemptWebflowEventResolver, final CasWebflowEventResolver serviceTicketRequestWebflowEventResolver, final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy, final CentralAuthenticationService centralAuthenticationService, + final OsfUrlProperties osfUrlProperties, + final OsfApiProperties osfApiProperties, final Map> authnDelegationClients ) { super( @@ -72,6 +175,8 @@ public OsfPrincipalFromNonInteractiveCredentialsAction( adaptiveAuthenticationPolicy ); this.centralAuthenticationService = centralAuthenticationService; + this.osfUrlProperties = osfUrlProperties; + this.osfApiProperties = osfApiProperties; this.authnDelegationClients = authnDelegationClients; } @@ -80,13 +185,14 @@ public OsfPrincipalFromNonInteractiveCredentialsAction( protected Credential constructCredentialsFromRequest(final RequestContext context) { final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context); - - // Check if credential already exists from delegated authentication final Credential credential = WebUtils.getCredential(context); + + // Check 1: is there an existing credential? if (credential != null) { LOGGER.debug("Existing credential found in context of type [{}]", credential.getClass()); if (credential instanceof ClientCredential) { final String clientName = ((ClientCredential) credential).getClientName(); + // Type 1: non-institution SSO (i.e. ORCiD) via pac4j authentication delegation using the OAuth protocol if (authnDelegationClients.get(NON_INSTITUTION_CLIENTS_PARAMETER_NAME).contains(clientName)) { LOGGER.debug( "Valid non-institution authn delegation client [{}] found with principal [{}]", @@ -95,44 +201,79 @@ protected Credential constructCredentialsFromRequest(final RequestContext contex ); return credential; } + // Type 2: institution SSO via pac4j authentication delegation using the CAS protocol if (authnDelegationClients.get(INSTITUTION_CLIENTS_PARAMETER_NAME).contains(clientName)) { LOGGER.debug( "Valid institution authn delegation client [{}] found with principal [{}]", clientName, credential.getId() ); - final OsfPostgresCredential osfPostgresCredential = new OsfPostgresCredential(); - osfPostgresCredential.setUsername(credential.getId()); - osfPostgresCredential.setInstitutionId(((ClientCredential) credential).getClientName()); - osfPostgresCredential.setRemotePrincipal(true); - osfPostgresCredential.setDelegationProtocol(DelegationProtocol.CAS_PAC4J); - return osfPostgresCredential; + final OsfPostgresCredential osfPostgresCredential = constructCredentialsFromPac4jAuthentication(context, clientName); + if (osfPostgresCredential != null) { + final OsfApiInstitutionAuthenticationResult remoteUserInfo = notifyOsfApiOfInstnAuthnSuccess(osfPostgresCredential); + osfPostgresCredential.setUsername(remoteUserInfo.getUsername()); + osfPostgresCredential.setInstitutionId(remoteUserInfo.getInstitutionId()); + WebUtils.removeCredential(context); + return osfPostgresCredential; + } + LOGGER.error("osfPostgresCredential is null for client [{}]", clientName); + return null; } - LOGGER.debug("Unsupported delegation client [{}]", clientName); + LOGGER.error("Unsupported delegation client [{}]", clientName); return null; } - LOGGER.debug("Unsupported delegation credential [{}]", credential.getClass().getSimpleName()); + LOGGER.error("Unexpected credential of type [{}]", credential.getClass().getSimpleName()); return null; } + LOGGER.debug("No valid client credential found in the request context: check shibboleth session."); - LOGGER.debug("No valid credential found in the request context."); - final OsfPostgresCredential osfPostgresCredential = new OsfPostgresCredential(); + // Check 2: is there an existing Shibboleth session? + final String shibbolethSession = request.getHeader(SHIBBOLETH_SESSION_HEADER); + if (StringUtils.isNotBlank(shibbolethSession)) { + // Type 3: institution sso via Shibboleth authentication using the SAML protocol + LOGGER.debug("Shibboleth session / header found in request context."); + final OsfPostgresCredential osfPostgresCredential = constructCredentialsFromShibbolethAuthentication(context, request); + + final OsfApiInstitutionAuthenticationResult remoteUserInfo = notifyOsfApiOfInstnAuthnSuccess(osfPostgresCredential); + osfPostgresCredential.setUsername(remoteUserInfo.getUsername()); + osfPostgresCredential.setInstitutionId(remoteUserInfo.getInstitutionId()); + if (StringUtils.isBlank(osfPostgresCredential.getInstitutionalIdentity())) { + LOGGER.warn( + "[SAML Shibboleth] Missing user's institutional identity: username={}, institutionId={}", + remoteUserInfo.getUsername(), + remoteUserInfo.getInstitutionId() + ); + } + return osfPostgresCredential; + } + LOGGER.debug("No valid shibboleth session found in request context: check username and verification key."); + + // Check 3: is there a pair of username and verification key in the request? final String username = request.getParameter(USERNAME_PARAMETER_NAME); final String verificationKey = request.getParameter(VERIFICATION_KEY_PARAMETER_NAME); if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(verificationKey)) { - osfPostgresCredential.setUsername(username); - osfPostgresCredential.setVerificationKey(verificationKey); - osfPostgresCredential.setRememberMe(true); - LOGGER.debug("User [{}] found in request w/ verificationKey", username); - return osfPostgresCredential; + // Type 4: automatic login with short-lived one-time verification key + return constructCredentialsFromUsernameAndVerificationKey(username, verificationKey); } - LOGGER.debug("No username or verification key found in the request parameters."); + LOGGER.debug("No valid username or verification key found in request parameters."); + + // Default when there is no non-interactive authentication available + // Type 5: return a null credential so that the login webflow will prepare login pages return null; } @Override protected Event doPreExecute(final RequestContext context) throws Exception { - return super.doPreExecute(context); + // super.doPreExecute() calls constructCredentialsFromRequest() whose exception must be caught and returned as a flow event + try { + return super.doPreExecute(context); + } catch (AccountException e) { + return new Event( + this, + CasWebflowConstants.TRANSITION_ID_AUTHENTICATION_FAILURE, + new LocalAttributeMap<>(CasWebflowConstants.TRANSITION_ID_ERROR, new AuthenticationException(e)) + ); + } } @Override @@ -152,4 +293,339 @@ protected Event doExecute(final RequestContext requestContext) { */ @Override protected void onError(final RequestContext context) {} + + /** + * This method allows the bean instance to perform validation of its overall configuration and final initialization + * when all bean properties have been set. + * + * @throws Exception in the event of mis-configuration or if initialization fails for any other reason + */ + @Override + public void afterPropertiesSet() throws Exception { + final File xslFile = ResourceUtils.getFile(osfApiProperties.getInstnAuthnXslLocation()); + final StreamSource xslStreamSource = new StreamSource(xslFile); + final TransformerFactory tFactory = TransformerFactory.newInstance(); + instnAuthnRespTransformer = tFactory.newTransformer(xslStreamSource); + super.afterPropertiesSet(); + } + + /** + * Construct an {@link OsfPostgresCredential} object form pac4j delegated authentication via the CAS client. + * + * @param context the request context + * @param clientName the client name + * @return an {@link OsfPostgresCredential} object + */ + private OsfPostgresCredential constructCredentialsFromPac4jAuthentication(final RequestContext context, final String clientName) { + + final Authentication authentication = WebUtils.getAuthentication(context); + final Principal principal = authentication.getPrincipal(); + final OsfPostgresCredential osfPostgresCredential = new OsfPostgresCredential(); + osfPostgresCredential.setRemotePrincipal(Boolean.TRUE); + osfPostgresCredential.setDelegationProtocol(DelegationProtocol.CAS_PAC4J); + osfPostgresCredential.getDelegationAttributes().put("Cas-Identity-Provider", clientName); + if (principal.getAttributes().size() > 0) { + for (final Map.Entry> entry : principal.getAttributes().entrySet()) { + final String attributeKey = entry.getKey(); + final List attributeValues = entry.getValue(); + if (attributeValues.isEmpty()) { + LOGGER.error( + "[CAS PAC4J] Empty-value attribute detected: '{}', '{}', '{}', '{}'", + clientName, + principal.getId(), + attributeKey, + attributeValues + ); + } else if (attributeValues.size() > 1) { + LOGGER.error( + "[CAS PAC4J] Multi-value attribute detected: '{}', '{}', '{}', '{}'", + clientName, + principal.getId(), + attributeKey, + attributeValues + ); + } else { + final Object firstAttributeValue = attributeValues.get(0); + LOGGER.debug( + "[CAS PAC4J] User's institutional identity '{}': '{}' w/ attribute '{}': '{}'", + clientName, + principal.getId(), + attributeKey, + firstAttributeValue + ); + if (firstAttributeValue instanceof String) { + LOGGER.info( + "[CAS PAC4J] Delegation attribute map updated: '{}', '{}', '{}', '{}'", + clientName, + principal.getId(), + attributeKey, + firstAttributeValue + ); + osfPostgresCredential.getDelegationAttributes().put( + attributeKey, + String.valueOf(firstAttributeValue) + ); + } else { + LOGGER.error( + "[CAS PAC4J] Attribute w/ non-string value: '{}', '{}', '{}', '{}', '{}'", + clientName, + principal.getId(), + entry.getKey(), + attributeKey, + firstAttributeValue.getClass().getName() + ); + } + } + } + return osfPostgresCredential; + } else { + LOGGER.error("[CAS PAC4J] No attributes for user '{} with client '{}'", principal.getId(), clientName); + return null; + } + } + + /** + * Construct an {@link OsfPostgresCredential} object from Shibboleth authentication. + * + * @param context the request context + * @param request the shibboleth authentication request with "auth-" prefixed headers + * @return an {@link OsfPostgresCredential} object + */ + private OsfPostgresCredential constructCredentialsFromShibbolethAuthentication( + final RequestContext context, + final HttpServletRequest request + ) { + final OsfPostgresCredential osfPostgresCredential = new OsfPostgresCredential(); + osfPostgresCredential.setDelegationProtocol(DelegationProtocol.SAML_SHIB); + osfPostgresCredential.setRemotePrincipal(Boolean.TRUE); + removeShibbolethSessionCookie(context); + + final String remoteUser = request.getHeader(REMOTE_USER); + if (StringUtils.isEmpty(remoteUser)) { + LOGGER.error("[SAML Shibboleth] Missing or empty Shibboleth header: {}", REMOTE_USER); + } else { + LOGGER.info("[SAML Shibboleth] User's institutional identity: '{}'", remoteUser); + } + for (final String headerName : Collections.list(request.getHeaderNames())) { + if (headerName.startsWith(ATTRIBUTE_PREFIX)) { + final String headerValue = request.getHeader(headerName); + LOGGER.debug( + "[SAML Shibboleth] User's institutional identity '{}' - auth header '{}': '{}'", + remoteUser, + headerName, + headerValue + ); + osfPostgresCredential.getDelegationAttributes().put( + headerName.substring(ATTRIBUTE_PREFIX.length()), + headerValue + ); + } + } + osfPostgresCredential.setInstitutionalIdentity(remoteUser); + return osfPostgresCredential; + } + + /** + * Construct an {@link OsfPostgresCredential} object with username and verification key. + * + * @param username the username in request parameters + * @param verificationKey the verification key in request parameters + * @return an {@link OsfPostgresCredential} object + */ + private OsfPostgresCredential constructCredentialsFromUsernameAndVerificationKey(final String username, final String verificationKey) { + final OsfPostgresCredential osfPostgresCredential = new OsfPostgresCredential(); + osfPostgresCredential.setUsername(username); + osfPostgresCredential.setVerificationKey(verificationKey); + osfPostgresCredential.setRememberMe(false); + LOGGER.debug("User [{}] found in request w/ verificationKey", username); + return osfPostgresCredential; + } + + /** + * Remove the shibboleth session cookie which is created by the Apache Shibboleth server after successful SAML authentication. + * + * @param context the Request Context + */ + private void removeShibbolethSessionCookie(final RequestContext context) { + final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context); + final Cookie[] cookies = request.getCookies(); + if (cookies != null) { + final HttpServletResponse response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context); + for (final Cookie cookie : cookies) { + if (cookie.getName().startsWith(SHIBBOLETH_COOKIE_PREFIX)) { + final Cookie shibbolethCookie = new Cookie(cookie.getName(), null); + shibbolethCookie.setMaxAge(0); + response.addCookie(shibbolethCookie); + } + } + } + } + + /** + * Extract delegated authentication data from the given {@link OsfPostgresCredential} object and normalize it as + * required by the OSF API institution authentication endpoint. + * + * @param credential the credential object bearing delegated authentication data + * + * @return a {@link JSONObject} object bearing data required by the OSF API institution authentication endpoint + * @throws ParserConfigurationException a parser configuration exception + * @throws TransformerException a transformer exception + */ + private JSONObject extractInstnAuthnDataFromCredential(final OsfPostgresCredential credential) + throws ParserConfigurationException, TransformerException { + + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + final DocumentBuilder builder = factory.newDocumentBuilder(); + final Document document = builder.newDocument(); + final Element rootElement = document.createElement("auth"); + document.appendChild(rootElement); + + final Element delegationProtocolAttr = document.createElement("attribute"); + delegationProtocolAttr.setAttribute("name", "Delegation-Protocol"); + delegationProtocolAttr.setAttribute("value", credential.getDelegationProtocol().getId()); + rootElement.appendChild(delegationProtocolAttr); + + for (final String key : credential.getDelegationAttributes().keySet()) { + final Element attribute = document.createElement("attribute"); + attribute.setAttribute("name", key); + attribute.setAttribute("value", credential.getDelegationAttributes().get(key)); + rootElement.appendChild(attribute); + } + + final DOMSource source = new DOMSource(document); + final StringWriter writer = new StringWriter(); + final StreamResult result = new StreamResult(writer); + instnAuthnRespTransformer.transform(source, result); + + return XML.toJSONObject(writer.getBuffer().toString()); + } + + /** + * Securely notify OSF API of a successful institution authentication between OSF CAS and an external IdP. This + * allows OSF API to either create a verified OSF account or find an existing active OSF account, and then assign + * institutional affiliation to them. OSF API returns HTTP 204 if successful and HTTP 401 / 403 if failed. Refer + * to the following code for the latest behavior of the institution authentication endpoint of OSF API: + * https://github.com/CenterForOpenScience/osf.io/blob/develop/api/institutions/authentication.py + * + * @param credential the credential object bearing delegated authentication data + * @return {@link OsfApiInstitutionAuthenticationResult} an object that stores institution and user info on success + * @throws AccountException if there is an issue with authentication data or if the OSF API request has failed + */ + private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( + final OsfPostgresCredential credential + ) throws AccountException { + + final JSONObject normalizedPayload; + try { + normalizedPayload = extractInstnAuthnDataFromCredential(credential); + } catch (final ParserConfigurationException | TransformerException e) { + LOGGER.error("[CAS XSLT] Failed to normalize attributes in the credential: {}", e.getMessage()); + throw new InstitutionSsoFailedException("Attribute normalization failure"); + } + + final JSONObject provider = normalizedPayload.optJSONObject("provider"); + if (provider == null) { + LOGGER.error("[CAS XSLT] Missing identity provider."); + throw new InstitutionSsoFailedException("Missing identity provider"); + } + final String institutionId = provider.optString("id").trim(); + if (institutionId.isEmpty()) { + LOGGER.error("[CAS XSLT] Empty identity provider"); + throw new InstitutionSsoFailedException("Empty identity provider"); + } + final JSONObject user = provider.optJSONObject("user"); + if (user == null) { + LOGGER.error("[CAS XSLT] Missing institutional user"); + throw new InstitutionSsoFailedException("Missing institutional user"); + } + final String username = user.optString("username").trim(); + final String fullname = user.optString("fullname").trim(); + final String givenName = user.optString("givenName").trim(); + final String familyName = user.optString("familyName").trim(); + final String isMemberOf = user.optString("isMemberOf").trim(); + if (username.isEmpty()) { + LOGGER.error("[CAS XSLT] Missing email (username) for user at institution '{}'", institutionId); + throw new InstitutionSsoFailedException("Missing email (username)"); + } + if (fullname.isEmpty() && (givenName.isEmpty() || familyName.isEmpty())) { + LOGGER.error("[CAS XSLT] Missing names: username={}, institution={}", username, institutionId); + throw new InstitutionSsoFailedException("Missing user's names"); + } + if (!isMemberOf.isEmpty()) { + LOGGER.info( + "[CAS XSLT] Secondary institution detected. SSO is '{}' and member is '{}'", + institutionId, + isMemberOf + ); + } + final String payload = normalizedPayload.toString(); + LOGGER.info( + "[CAS XSLT] All attributes checked: username={}, institution={}, member={}", + username, + institutionId, + isMemberOf + ); + LOGGER.debug( + "[CAS XSLT] All attributes checked: username={}, institution={}, member={}, normalizedPayload={}", + username, + institutionId, + isMemberOf, + payload + ); + + final String jweString; + try { + final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .subject(username) + .claim("data", payload) + .expirationTime(new Date(new Date().getTime() + SIXTY_SECONDS)) + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet); + final JWSSigner signer = new MACSigner(osfApiProperties.getInstnAuthnJwtSecret().getBytes()); + signedJWT.sign(signer); + final JWEObject jweObject = new JWEObject( + new JWEHeader.Builder(JWEAlgorithm.DIR, EncryptionMethod.A256GCM) + .contentType("JWT") + .build(), + new Payload(signedJWT)); + jweObject.encrypt(new DirectEncrypter(osfApiProperties.getInstnAuthnJweSecret().getBytes())); + jweString = jweObject.serialize(); + } catch (final JOSEException e) { + LOGGER.error( + "[OSF API] Notify Remote Principal Authenticated Failed: Payload Error - {}", + e.getMessage() + ); + throw new InstitutionSsoFailedException("OSF CAS failed to build JWT / JWE payload for OSF API"); + } + + try { + final HttpResponse httpResponse = Request.Post(osfApiProperties.getInstnAuthnEndpoint()) + .addHeader(new BasicHeader("Content-Type", "text/plain")) + .bodyString(jweString, ContentType.APPLICATION_JSON) + .execute() + .returnResponse(); + final int statusCode = httpResponse.getStatusLine().getStatusCode(); + LOGGER.info( + "[OSF API] Notify Remote Principal Authenticated Response: username={} statusCode={}", + username, + statusCode + ); + if (statusCode != HttpStatus.SC_NO_CONTENT) { + final String responseString = new BasicResponseHandler().handleResponse(httpResponse); + LOGGER.error( + "[OSF API] Notify Remote Principal Authenticated Failed: statusCode={}, body={}", + statusCode, + responseString + ); + throw new InstitutionSsoFailedException("OSF API failed to process CAS request"); + } + return new OsfApiInstitutionAuthenticationResult(username, institutionId); + } catch (final IOException e) { + LOGGER.error( + "[OSF API] Notify Remote Principal Authenticated Failed: Communication Error - {}", + e.getMessage() + ); + throw new InstitutionSsoFailedException("Communication Error between OSF CAS and OSF API"); + } + } } diff --git a/src/main/java/io/cos/cas/osf/web/flow/support/OsfCasWebflowConstants.java b/src/main/java/io/cos/cas/osf/web/flow/support/OsfCasWebflowConstants.java index ea6f877..cba0e49 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/support/OsfCasWebflowConstants.java +++ b/src/main/java/io/cos/cas/osf/web/flow/support/OsfCasWebflowConstants.java @@ -2,7 +2,7 @@ /** * This is {@link OsfCasWebflowConstants}, which expands the default {@link org.apereo.cas.web.flow.CasWebflowConstants} - * interface by adding OSF CAS customized action, state and view IDs. + * interface by adding OSF CAS customized action, state, transition and view IDs as well as names web flow parameters. * * @author Longze Chen * @since 20.0.0 @@ -10,25 +10,43 @@ public interface OsfCasWebflowConstants { - String ACTION_ID_OSF_DEFAULT_LOGIN_CHECK = "osfDefaultLoginCheckAction"; + String FLOW_PARAMETER_OSF_URL = "osfUrl"; - String STATE_ID_OSF_DEFAULT_LOGIN_CHECK = "osfDefaultLoginCheck"; + String ACTION_ID_OSF_PRE_INITIAL_FLOW_SETUP = "osfCasPreInitialFlowSetupAction"; - String TRANSITION_ID_USERNAME_PASSWORD_LOGIN = "continueToUsernamePasswordLogin"; + String ACTION_ID_OSF_NON_INTERACTIVE_AUTHENTICATION_CHECK = "osfNonInteractiveAuthenticationCheckAction"; + + String ACTION_ID_OSF_DEFAULT_LOGIN_CHECK = "osfDefaultLoginCheckAction"; String ACTION_ID_OSF_INSTITUTION_LOGIN_CHECK = "osfInstitutionLoginCheckAction"; - String STATE_ID_OSF_INSTITUTION_LOGIN_CHECK = "osfInstitutionLoginCheck"; + String TRANSITION_ID_USERNAME_PASSWORD_LOGIN = "continueToUsernamePasswordLogin"; String TRANSITION_ID_INSTITUTION_LOGIN = "switchToInstitutionLogin"; + String TRANSITION_ID_UNSUPPORTED_INSTITUTION_LOGIN = "switchToUnsupportedInstitutionLogin"; + String TRANSITION_ID_ORCID_LOGIN_AUTO_REDIRECT = "autoRedirectToOrcidLogin"; + String TRANSITION_ID_DEFAULT_SERVICE_LOGIN_AUTO_REDIRECT = "autoRedirectToDefaultServiceLogin"; + + String STATE_ID_OSF_NON_INTERACTIVE_AUTHENTICATION_CHECK = "osfNonInteractiveAuthenticationCheck"; + + String STATE_ID_OSF_DEFAULT_LOGIN_CHECK = "osfDefaultLoginCheck"; + + String STATE_ID_OSF_INSTITUTION_LOGIN_CHECK = "osfInstitutionLoginCheck"; + + String VIEW_ID_INSTITUTION_SSO_INIT = "casInstitutionLoginView"; + + String VIEW_ID_UNSUPPORTED_INSTITUTION_SSO_INIT = "casUnsupportedInstitutionLoginView"; + String VIEW_ID_ORCID_LOGIN_AUTO_REDIRECT = "casAutoRedirectToOrcidLoginView"; - String ACTION_ID_OSF_NON_INTERACTIVE_AUTHENTICATION_CHECK = "osfNonInteractiveAuthenticationCheckAction"; + String VIEW_ID_DEFAULT_SERVICE_LOGIN_AUTO_REDIRECT = "casAutoRedirectToDefaultServiceLoginView"; - String STATE_ID_OSF_NON_INTERACTIVE_AUTHENTICATION_CHECK = "osfNonInteractiveAuthenticationCheck"; + String VIEW_ID_ONE_TIME_PASSWORD_REQUIRED = "casTwoFactorLoginView"; + + String VIEW_ID_TERMS_OF_SERVICE_CONSENT_REQUIRED = "casTermsOfServiceConsentView"; String VIEW_ID_ACCOUNT_NOT_CONFIRMED_OSF = "casAccountNotConfirmedOsfView"; @@ -38,7 +56,5 @@ public interface OsfCasWebflowConstants { String VIEW_ID_INVALID_VERIFICATION_KEY = "casInvalidVerificationKeyView"; - String VIEW_ID_ONE_TIME_PASSWORD_REQUIRED = "casTwoFactorLoginView"; - - String VIEW_ID_INSTITUTION_SSO_NOT_IMPLEMENTED = "casInstitutionSsoNotImplementedView"; + String VIEW_ID_INSTITUTION_SSO_FAILED = "casInstitutionSsoFailedView"; } diff --git a/src/main/java/io/cos/cas/osf/web/support/OsfApiInstitutionAuthenticationResult.java b/src/main/java/io/cos/cas/osf/web/support/OsfApiInstitutionAuthenticationResult.java new file mode 100644 index 0000000..8e5b26b --- /dev/null +++ b/src/main/java/io/cos/cas/osf/web/support/OsfApiInstitutionAuthenticationResult.java @@ -0,0 +1,29 @@ +package io.cos.cas.osf.web.support; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; + +/** + * This is {@link OsfApiInstitutionAuthenticationResult}. + * + * @author Longze Chen + * @since 21.0.0 + */ +@AllArgsConstructor +@Getter +@NoArgsConstructor +@ToString +@Setter +public class OsfApiInstitutionAuthenticationResult implements Serializable { + + private static final long serialVersionUID = 3971349776123204760L; + + private String username; + + private String institutionId; +} diff --git a/src/main/java/io/cos/cas/osf/web/support/OsfCasLoginContext.java b/src/main/java/io/cos/cas/osf/web/support/OsfCasLoginContext.java index 32fa640..35bdd22 100644 --- a/src/main/java/io/cos/cas/osf/web/support/OsfCasLoginContext.java +++ b/src/main/java/io/cos/cas/osf/web/support/OsfCasLoginContext.java @@ -15,7 +15,7 @@ * and retrieved from the flow context conveniently. * * @author Longze Chen - * @since 20.0.0 + * @since 20.1.0 */ @AllArgsConstructor @Getter @@ -26,7 +26,13 @@ public class OsfCasLoginContext implements Serializable { private static final long serialVersionUID = 7523144720609509742L; - private String serviceUrl; + /** + * The encoded service URL provided by the "service=" query param in the request URL. + * + * This attribute is deprecated and should be removed since 1) ThymeLeaf handles URL building elegantly in the template and 2) both of + * the flow parameters "service.originalUrl" and "originalUrl" stores the current service information. + */ + private String encodedServiceUrl; private String handleErrorName; @@ -34,22 +40,39 @@ public class OsfCasLoginContext implements Serializable { private String institutionId; + private boolean unsupportedInstitutionLogin; + private boolean orcidRedirect; private String orcidLoginUrl; + private boolean defaultService; + + /** + * The default service URL that uses OSF login endpoint with OSF home as destination. + * + * e.g. http(s)://[OSF Domain]/login?next=[encoded version of http(s)://[OSF Domain]/] + */ + private String defaultServiceUrl; + public OsfCasLoginContext ( - final String serviceUrl, + final String encodedServiceUrl, final boolean institutionLogin, final String institutionId, + final boolean unsupportedInstitutionLogin, final boolean orcidRedirect, - final String orcidLoginUrl + final String orcidLoginUrl, + final boolean defaultService, + final String defaultServiceUrl ) { - this.serviceUrl = serviceUrl; + this.encodedServiceUrl = encodedServiceUrl; this.handleErrorName = null; this.institutionLogin = institutionLogin; this.institutionId = institutionId; + this.unsupportedInstitutionLogin = unsupportedInstitutionLogin; this.orcidRedirect = orcidRedirect; this.orcidLoginUrl = orcidLoginUrl; + this.defaultService = defaultService; + this.defaultServiceUrl = defaultServiceUrl; } } diff --git a/src/main/java/org/apereo/cas/configuration/model/core/authentication/AuthenticationProperties.java b/src/main/java/org/apereo/cas/configuration/model/core/authentication/AuthenticationProperties.java index 82295f6..d8d7b1f 100644 --- a/src/main/java/org/apereo/cas/configuration/model/core/authentication/AuthenticationProperties.java +++ b/src/main/java/org/apereo/cas/configuration/model/core/authentication/AuthenticationProperties.java @@ -1,6 +1,8 @@ package org.apereo.cas.configuration.model.core.authentication; +import io.cos.cas.osf.configuration.model.OsfApiProperties; import io.cos.cas.osf.configuration.model.OsfPostgresAuthenticationProperties; +import io.cos.cas.osf.configuration.model.OsfUrlProperties; import lombok.Getter; import lombok.Setter; @@ -99,6 +101,18 @@ public class AuthenticationProperties implements Serializable { @NestedConfigurationProperty private JsonResourceAuthenticationProperties json = new JsonResourceAuthenticationProperties(); + /** + * OSF URL settings. + */ + @NestedConfigurationProperty + private OsfUrlProperties osfUrl = new OsfUrlProperties(); + + /** + * OSF API settings. + */ + @NestedConfigurationProperty + private OsfApiProperties osfApi = new OsfApiProperties(); + /** * OSF Postgres authentication settings. */ diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 2a9ad09..2a52898 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1,8 +1,7 @@ ######################################################################################################################## -# Default messages.properties as of 6.2.x, see: -# https://github.com/apereo/cas/blob/6.2.x/support/cas-server-support-thymeleaf/src/main/resources/messages.properties -# -# Note: customized messages are commented out and their respective customizations are added to the end of this file +# Default messages.properties as of 6.2.x, see: # +# https://github.com/apereo/cas/blob/6.2.x/support/cas-server-support-thymeleaf/src/main/resources/messages.properties # +# Note: customized messages are commented out and their respective customizations are added to the end of this file # ######################################################################################################################## # screen.welcome.welcome=Congratulations on bringing CAS online! To learn how to authenticate, please review the default authentication handler configuration. @@ -170,7 +169,7 @@ screen.error.page.title.requestunsupported=Error - Unsupported Request screen.error.page.accessdenied=Access Denied screen.error.page.permissiondenied=You do not have permission to view this page. screen.error.page.requestunsupported=The request type or syntax is not supported. -screen.error.page.loginagain=Login Again +# screen.error.page.loginagain=Login Again screen.error.page.notfound=Page Not Found screen.error.page.doesnotexist=The page you are attempting to access does not exist at the moment. screen.error.page.authdenied=Authorization Denied @@ -402,7 +401,7 @@ screen.mfaUnavailable.message=CAS was unable to reach your configured MFA provid # Login View ##################################################################### #Resources Labels -# cas.login.pagetitle=Login +cas.login.pagetitle=Login cas.login.resources.header=Resources cas.login.resources.wiki=Documentation cas.login.resources.endpoints=Actuator Endpoints @@ -485,17 +484,15 @@ passwordless.error.invalid.user=Provided username does not carry enough contact noop.message=This's a noop message. # -######################################################################################################################## -# -# End of default messages.properties -# -######################################################################################################################## +###################################### +# End of default messages.properties # +###################################### -######################################################################################################################## -# OSF CAS customized messages.properties -######################################################################################################################## +########################################## +# OSF CAS customized messages.properties # +########################################## # # Header # @@ -504,22 +501,22 @@ noop.message=This's a noop message. # App drawer (left) # cas.drawer.title=OSF CAS -cas.drawer.subtitle=OSF CAS is the central authentication and authorization service for OSF +cas.drawer.subtitle=OSF CAS is the central authentication service for OSF cas.login.resources.cas.home=CAS Home cas.login.resources.osf.home=OSF Home cas.login.resources.osf.preprints=OSF Preprints cas.login.resources.osf.registries=OSF Registries cas.login.resources.osf.meetings=OSF Meetings cas.login.resources.osf.institutions=OSF Institutions +cas.login.resources.osf.search=Search cas.login.resources.osf.support=OSF Support -cas.login.resources.cos.donate=Donate -cas.login.resources.cos.home=COS Home +cas.login.resources.osf.donate=Donate # # App notifications (right) # # Footer # -copyright.cos=Copyright © 2011 – 2020 \ +copyright.cos=Copyright © 2011 – 2021 \ Center for Open Science | \ Terms of Use | \ Privacy Policy | \ @@ -529,53 +526,106 @@ copyright.cos=Copyright © 2011 – 20 # screen.welcome.label.loginwith=Sign in through external identity providers # +# Texts and messages that are shared across all pages +# +screen.generic.label.source=Source: +screen.generic.button.wip=One moment please ... +screen.generic.link.support=Need help signing in? +# # Login page and login form submission # -cas.login.pagetitle=Sign in -screen.welcome.instructions=Sign in with your OSF account to continue +cas.login.title=Sign in +screen.welcome.tips=Sign in with your OSF account to continue screen.welcome.label.email=Email screen.welcome.label.email.accesskey=e screen.welcome.label.password=Password screen.welcome.label.password.accesskey=p screen.welcome.button.login=Sign in -screen.rememberme.checkbox.title=Stay signed in -screen.pm.button.forgotpwd=Forgot your password? -screen.pm.button.createaccount=Create an OSF account -screen.pm.button.backtoosf=Back to OSF -screen.delegation.button.orcid=Sign in with ORCiD -screen.delegation.button.institution=Sign in through institution -screen.delegation.heading.orcidredirect=Redirecting to ORCiD -# -# Two factor and login form submission -# -cas.twofactor.pagetitle=Sign in -screen.twofactor.instructions.top=Enter your one-time password to finish login +screen.welcome.checkbox.rememberme=Stay signed in +screen.welcome.link.resetpassword=Reset password +screen.welcome.button.orcidlogin=Sign in with ORCiD +screen.welcome.button.institutionlogin=Sign in via institution +screen.orcidredirect.heading=Redirecting to ORCiD +screen.defaultserviceredirect.heading=Redirecting to OSF login +# +# Two factor login form submission +# +screen.twofactor.title=Sign in +screen.twofactor.tips.top=Enter your one-time password to finish login +screen.twofactor.label.email=Your OSF account +screen.twofactor.label.email.accesskey=a screen.twofactor.label.onetimepassword=One-time password (6-digit) screen.twofactor.label.onetimepassword.accesskey=o screen.twofactor.button.verify=Verify -screen.twofactor.button.verifywip=One moment please... -screen.twofactor.button.cancel=Cancel -screen.twofactor.instructions.bottom=Open the two-factor authentication app on your device to view your authentication code and verify your identity. +screen.twofactor.link.cancel=Cancel +screen.twofactor.tips.bottom=Open the two-factor authentication app on your device to view your authentication code and verify your identity. +# +# Terms of service consent check login form submission +# +screen.tosconsent.title=Terms of service +screen.tosconsent.tips=Terms of use and privacy policy +screen.tosconsent.message.p1=You are seeing this page, either because this is your first-time signing into OSF via your \ + institution, or because we have recently updated the terms that you need to review. +screen.tosconsent.message.p2=You must agree to our \ + Terms of Use and \ + Privacy Policy \ + to finish login. Please read them carefully! +screen.tosconsent.message.p3=Contact OSF Support should you have any questions. +screen.tosconsent.checkbox.title=I have read and agree to these terms. +screen.tosconsent.button.agree=Continue +screen.tosconsent.link.cancel=Cancel and go back to OSF +# +# Institution login page +# +screen.institutionlogin.title=Institution SSO +screen.institutionlogin.heading=Sign in through your institution +screen.institutionlogin.message.select=If your institution has partnered with OSF, please select its name below and sign in with your institutional credentials. If you do not currently have an OSF account, this will create one for you. +screen.institutionlogin.message.auto=Your institution has partnered with OSF. Please continue to sign in with your institutional credentials. If you do not currently have an OSF account, this will create one for you. +screen.institutionlogin.heading.select=Select your institution +screen.institutionlogin.heading.auto=Your institution +screen.institutionlogin.link.select=Not your institution? +screen.institutionlogin.link.unsupported=I can't find my institution +screen.institutionlogin.button.submit=Sign in +screen.institutionlogin.osf=Sign in with your OSF account +# +# Unsupported Institution +# +screen.unsupp-instn.heading=I can't find my institution +screen.unsupp-instn.message=Signing into the OSF with institutional credentials is enabled for OSF Institutions members. If your institution is not yet an OSFI member, choose one of the following sign-in methods. +screen.unsupp-instn.existing.heading=I already have an OSF account +screen.unsupp-instn.existing.message=If you have already have an OSF password, you can sign in to the OSF +screen.unsupp-instn.existing.button=Sign in with OSF +screen.unsupp-instn.existing.setpw.message=If you have not yet set an OSF password for your account, create a password +screen.unsupp-instn.existing.setpw.button=Set a password +screen.unsupp-instn.existing.setpw.label.email=Email +screen.unsupp-instn.existing.setpw.label.email.accesskey=e # # Generic login and logout success page # -screen.generic.logout.header=Signed-out -screen.generic.logout.heading=Logout successful -screen.generic.logout.message=You have successfully logged out of the OSF Central Authentication Service (CAS). You may continue to OSF signed-out or return to the sign-in page. -screen.generic.login.header=Signed-in -screen.generic.login.heading=Login successful -screen.generic.login.message=You have successfully logged into the OSF Central Authentication Service (CAS). You may continue to OSF signed-in or log out and go back to OSF home page. -screen.generic.button.backtoosf=Continue to OSF -screen.generic.button.backtologin=Return to sign-in -screen.generic.button.logout=Log out -screen.generic.button.viewdetails=View authentication details -screen.generic.button.hidedetails=Hide authentication details +screen.generic.logoutsuccess.title=Signed-out +screen.generic.logoutsuccess.tip=Oops! Redirection didn't happen ... +screen.generic.logoutsuccess.heading=Logout successful +screen.generic.logoutsuccess.message=You have successfully logged out of OSF. However, you are seeing this page \ + because the automatic redirection somehow didn't happen. Please click the button below to go back to OSF. +screen.generic.logoutsuccess.button.continue=Back to OSF +screen.generic.logoutsuccess.link.login=Login again +# +screen.generic.loginsuccess.title=Signed-in +screen.generic.loginsuccess.tips=Oops! Redirection didn't happen ... +screen.generic.loginsuccess.heading=Login successful +screen.generic.loginsuccess.message=You have successfully logged into OSF! However, you are seeing this page because \ + the automatic redirection somehow didn't happen. Please click the button below to continue to OSF. +screen.generic.loginsuccess.button.continue=Continue to OSF +screen.generic.loginsuccess.link.cancel=Exit login +screen.generic.loginsuccess.button.show=View authentication details +screen.generic.loginsuccess.button.hide=Hide authentication details # # Authentication exception messages on the login form submission page (inline / pop-up) # username.required=Email is required. password.required=Password is required. oneTimePassword.required=One-time password is required. +termsOfServiceChecked.required=Terms of service consent is required. authenticationFailure.AccountDisabledException=This account has been disabled. authenticationFailure.AccountNotFoundException=The email or password you entered is incorrect. authenticationFailure.AccountNotConfirmedOsfException=The account you tried to log in to has not been confirmed. @@ -587,12 +637,16 @@ authenticationFailure.InvalidPasswordException=The email or password you entered authenticationFailure.InvalidVerificationKeyException=The verification key you entered is incorrect. authenticationFailure.InvalidUserStatusException=The account you tried to log in to is not active. authenticationFailure.OneTimePasswordRequiredException= +authenticationFailure.TermsOfServiceConsentRequiredException= # # Authentication exception messages in stand-alone exception views # -screen.authnerror.instructions=Oops! Something went wrong ... +screen.error.page.loginagain=Log in again +screen.authnerror.title=Login Error +screen.authnerror.tips=Oops! Something went wrong ... +screen.authnerror.tips.devmode=Developer mode only !!! screen.authnerror.button.resendosfconfirmation=Resend confirmation email -screen.authnerror.button.backtoosf=Exit and go back to OSF +screen.authnerror.button.backtoosf=Exit login screen.accountdisabled.heading=Account disabled screen.accountdisabled.message=The OSF account associated with the email has been disabled. Please contact OSF Support to regain access. @@ -604,14 +658,14 @@ screen.accountnotconfirmedidp.message=The OSF account associated with the email screen.accountnotconfirmedosf.heading=Account not confirmed screen.accountnotconfirmedosf.message=The OSF account associated with the email has been registered but not confirmed. \ Please check your email (and spam folder) or click the button below to resend your confirmation email. -screen.unavailable.header=CAS Error +screen.servererror.title=Server Error screen.unavailable.heading=CAS unavailable screen.unavailable.message=Your request cannot be completed at this time. Please return to OSF and try again later. If \ this issue persists, please contact OSF Support with \ a copy of the error details below. screen.unavailable.button.viewdetails=View error details screen.unavailable.button.hidedetails=Hide error details -screen.service.error.header=Service Error +screen.service.error.title=Service Error screen.service.error.heading=Service not authorized screen.service.error.message=The application you attempted to authenticate to is not authorized to use OSF CAS. Please \ return to OSF and try again. If this issue persists, please contact OSF Support. -screen.institutionssonotimplemented.heading=Institution login error -screen.institutionssonotimplemented.message=The implementation of institution login page is work-in-progress, please \ - check back later ... -# -# OSF URLs -# -# OSF -# -osf.home.url=https://staging3.osf.io/ -osf.logout.url=https://staging3.osf.io/logout -osf.resend.osf.confirmation.url=https://staging3.osf.io/resend/ -osf.forgot.password.url=https://staging3.osf.io/forgotpassword/ -osf.create.account.url=https://staging3.osf.io/register/ -# -# API -# -# Other -# -osf.preprints.home.url=https://staging3.osf.io/preprints/ -osf.registries.home.url=https://staging3.osf.io/registries/ -osf.institutions.home.url=https://staging3.osf.io/institutions/ -osf.meetings.home.url=https://staging3.osf.io/meetings/ -osf.support.url=https://staging3.osf.io/support/ -osf.cos.donation.url=https://www.cos.io/about/support-cos/ -osf.cos.home.url=https://www.cos.io/ -# -######################################################################################################################## -# -# Enf of OSF CAS customized messages.properties -# -######################################################################################################################## +screen.institutionssofailed.title=Institution SSO Error +screen.institutionssofailed.heading=Institution login failed +screen.institutionssofailed.message=Your request cannot be completed at this time. Please \ + return to OSF and try again later.

If the issue persists, \ + check with your institution to verify your account is entitled to authenticate to OSF.

If you believe this \ + is in error, please contact Support for help. +################################################# +# Enf of OSF CAS customized messages.properties # +# ############################################### diff --git a/src/main/resources/static/css/cas.css b/src/main/resources/static/css/cas.css index 5e6609d..5331674 100644 --- a/src/main/resources/static/css/cas.css +++ b/src/main/resources/static/css/cas.css @@ -1,3 +1,7 @@ +/************************************************** + * Start of default styles as of Apereo CAS 6.2.x * + **************************************************/ + /* Root / Reset */ :root { @@ -114,15 +118,19 @@ header>nav .cas-brand .cas-logo { border: none; } -@media screen and (max-width: 767.99px) { - .login-section { - border-right: none; - border-bottom: 1px solid rgba(0, 0, 0, .2); - border-bottom: var(--cas-theme-border-light, 1px solid rgba(0, 0, 0, .2)); - max-width: none; - padding: 0 1.5rem; - } -} +/** + * Disabled for OSF CAS. + * + * @media screen and (max-width: 767.99px) { + * .login-section { + * border-right: none; + * border-bottom: 1px solid rgba(0, 0, 0, .2); + * border-bottom: var(--cas-theme-border-light, 1px solid rgba(0, 0, 0, .2)); + * max-width: none; + * padding: 0 1.5rem; + * } + * } + */ .close { font-size: 1.5rem; @@ -132,7 +140,6 @@ header>nav .cas-brand .cas-logo { text-shadow: 0 1px 0 #fff; text-transform: none; text-decoration: none; - ; } button.close { @@ -219,11 +226,15 @@ button.close { margin-right: 0.375rem; } -@media screen and (max-width: 767.99px) { - .logout-banner { - width: 100%; - } -} +/** + * Disabled for OSF CAS. + * + * @media screen and (max-width: 767.99px) { + * .logout-banner { + * width: 100%; + * } + * } + */ .cas-footer { font-size: 0.75rem; @@ -584,19 +595,28 @@ button.close { border-radius: 4px; } -/* customized styles for OSF CAS */ +/************************************************ + * End of default styles as of Apereo CAS 6.2.x * + ************************************************/ + +/****************************************** + * Start of customized styles for OSF CAS * + ******************************************/ :root { --cas-theme-osf-navbar: #263947; - --cas-theme-osf-surface: #F7F7F7; - --cas-theme-osf-footer: #EFEFEF; + --cas-theme-osf-surface: #f7f7f7; + --cas-theme-osf-footer: #efefef; + --cas-theme-osf-grey: #eeeeee; --cas-theme-osf-green: #357935; --cas-theme-osf-blue: #1b6d85; --cas-theme-osf-red: #b52b27; + --cas-theme-osf-disabled: #eeeeee; + --cas-theme-osf-disabled-dark: #cccccc; --cas-theme-primary: var(--cas-theme-osf-navbar, #263947); --cas-theme-danger: var(--cas-theme-osf-red, #b52b27); --mdc-theme-primary: var(--cas-theme-primary, #263947); - --mdc-theme-surface: var(--cas-theme-osf-surface, #F7F7F7); + --mdc-theme-surface: var(--cas-theme-osf-surface, #f7f7f7); } body { @@ -605,6 +625,12 @@ body { url(/images/page-background.png) center center no-repeat; background-size: cover; background-blend-mode: normal; + backdrop-filter: brightness(0.8); + min-width: 360px; +} + +.mdc-typography { + font-family: 'Open Sans','Helvetica Neue',sans-serif!important; } .mdc-top-app-bar__section--align-start, @@ -618,122 +644,6 @@ body { justify-content: center; } -.cas-footer { - font-size: 0.75rem; - background: var(--cas-theme-osf-footer, #efefef); -} - -.cas-brand-drawer { - margin: 2rem 0 0.5rem 0; -} - -.login-section { - max-width: 36rem; - min-width: fit-content; -} - -@media screen and (max-width: 767.99px) { - .login-section { - padding: 1rem 1.5rem; - } -} - -#serviceui { - background-color: transparent; -} - -.service-ui { - margin-top: 1rem!important; - margin-bottom: 1rem!important; -} - -.service-ui .service-ui-logo { - max-height: 5rem; -} - -.service-ui .service-ui-info { - margin-left: 1rem!important; - margin-right: 1rem!important; -} - -.service-ui-info .service-ui-name { - margin: 1rem 0; -} - -.service-ui-info.service-ui-desc { - font-size: 1rem; - margin: 1rem 0; -} - -.text-with-mdi, -.text-without-mdi { - font-size: 1rem; - margin: 0.5rem 0; -} - -.login-section .text-with-mdi { - white-space: nowrap; -} - -.mdi-before-text { - vertical-align: middle; - margin-right: 0.5rem; -} - -.delegation-row { - margin: 0.5rem 0; -} - -.delegation-button { - position: relative; - outline: none; - -webkit-appearance: none; - width: 100%; - text-decoration: none; - font-size: 1rem; - background-image: linear-gradient(to bottom, #f7f7f7, #d7d7d7); - border: 1px solid var(--cas-theme-primary, #153e50); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - display: inline-block; - padding: 10px 0; - text-align: center; -} - -.delegation-button:hover { - cursor: pointer; - -webkit-box-shadow: 0px 0px 5px 0px #ababab; - -moz-box-shadow: 0px 0px 5px 0px #ababab; - box-shadow: 0px 0px 5px 0px #ababab; -} - -.delegation-button:active { - background-image: linear-gradient(to top, #f7f7f7, #d7d7d7); -} - -.delegation-button-logo { - position: absolute; - left: 0; - top: 0; - border-right: 1px solid var(--cas-theme-primary, #153e50); - padding: 6px; - height: 38px -} - -.delegation-button-label { - font-size: 1rem; - color: black; - opacity: 0.75; - white-space: nowrap; - padding-left: 39px; -} - -.cas-field-primary { - margin-bottom: 1rem!important; - margin-top: 0.5rem!important; -} - .hr-text { line-height: 1rem; position: relative; @@ -746,11 +656,6 @@ body { font-size: 1rem; } -.login-error-card .hr-text, -.login-generic-card .hr-text { - margin: 1.5rem 0; -} - .hr-text::before { content: ''; background: linear-gradient(to right, transparent, black, var(--cas-theme-osf-surface, #f7f7f7)); @@ -789,11 +694,25 @@ body { width: 100%; min-width: fit-content; text-transform: none; + white-space: nowrap; + height: 56px; } .form-button-inline .mdc-button { flex-basis: 48%; margin: 0.25rem 0; + padding: 0 8px; +} + +.form-button .button-osf-navbar, +.form-button-inline .button-osf-navbar { + background-color: var(--cas-theme-osf-navbar, #263947); +} + +.form-button .button-osf-grey, +.form-button-inline .button-osf-grey { + background-color: var(--cas-theme-osf-grey, #eeeeee); + box-shadow: 0px 5px 4px 0px rgba(0, 0, 0, 0.2), 0px 4px 4px 0px rgba(0, 0, 0, 0.14), 0px 3px 8px 0px rgba(0, 0, 0, 0.12); } .form-button .button-osf-green, @@ -811,186 +730,479 @@ body { background-color: var(--cas-theme-osf-red, #b52b27); } -.login-error-inline { - margin: 0.25rem 0; +.form-button .button-osf-disabled, +.form-button-inline .button-osf-disabled { + background-color: var(--cas-theme-osf-disabled, #EFEFEF); } -.login-error-card, -.login-generic-card { +.mdc-top-app-bar__row, +.mdc-top-app-bar__row .mdc-top-app-bar__section { min-width: fit-content; } +.mdc-top-app-bar__row .cas-brand, +.mdc-top-app-bar__row .cas-brand-text { + margin-right: 0.5em!important; + margin-left: 0.5em!important; +} + +.cas-brand-text .navbar-link { + text-decoration: none; + color: #F7F7F7; +} + +.cas-brand-text .navbar-link:hover, +.cas-brand-text .navbar-link:visited, +.cas-brand-text .navbar-link:active { + color: #F7F7F7; +} + +.mdc-top-app-bar__row .button-osf-disabled { + opacity: 0.8; + cursor: not-allowed; + color: var(--cas-theme-osf-disabled-dark, #cccccc); +} + +.mdc-top-app-bar__row .cas-brand-name { + font-weight: bold; + color: #F7F7F7; +} + +.mdc-top-app-bar__row .hidden-narrow, +.service-ui .osf-shield-with-name .hidden-narrow { + font-weight: normal; +} + +.login-section, +.login-error-card { + border: none; + width: 512px; +} + +.login-error-card { + padding: 2rem 2.5rem; + flex: 1; +} + +#serviceui { + background-color: transparent; +} + +.service-ui { + margin-top: 1rem!important; + margin-bottom: 1rem!important; +} + +.service-ui .service-ui-logo { + max-height: 56px; + max-width: 360px; +} + +.service-ui .service-ui-logo-branded { + max-height: 48px; + max-width: 360px; +} + +.service-ui .osf-shield-with-name { + margin: 0 auto; + padding-bottom: 1rem; +} + +.osf-shield-with-name-branded { + width: 1px; +} + +.osf-shield-with-name .service-ui-logo { + padding-right: 0.5rem; +} + +.osf-shield-with-name .service-ui-logo-branded { + padding-right: 1rem; +} + +.osf-shield-with-name .service-ui-name { + font-size: 2.25rem; + font-weight: bold; +} + +.osf-shield-with-name .service-ui-name-branded { + font-size: 2rem; + font-weight: normal; + white-space: nowrap; +} + +.text-with-mdi, +.text-without-mdi { + font-size: 1rem; + margin: 0.5rem 0; +} + +.mdi-before-text { + font-size: inherit; + vertical-align: text-bottom; + margin-right: 0.5rem; +} + +.mdi-before-text:before { + font-size: inherit; +} + +.title-danger { + color: var(--cas-theme-osf-red, #b52b27); +} + +.text-no-wrap { + white-space: nowrap; +} + +.text-bold { + font-weight: bold; +} + +.text-large { + font-size: 1.125rem; +} + +.margin-large-vertical { + margin: 1rem 0; +} + +.hidden-button, +.hidden-details { + display: none; +} + +.form-button-inline .delegation-button-logo { + position: absolute; + left: 4px; + top: 12px; + padding: 6px; + height: 36px; +} + +.form-button-inline .delegation-button-label { + font-size: 0.875rem; + color: black; + white-space: nowrap; + padding-left: 28px; +} + +.login-error-card .form-button { + padding-top: 1rem; +} + +.login-section .mdc-button, +.login-section .mdc-text-field, +.login-error-card .mdc-button, +.login-error-card .mdc-text-field { + font-size: 1rem; +} + .login-error-card .pre-formatted-small pre { font-size: 0.75rem; white-space: pre-wrap; } -.banner-generic, -.banner-danger { - min-width: fit-content; +.login-section .background-disabled { + background-color: var(--cas-theme-osf-disabled-dark, #cccccc); } -.banner-dismissible { - background-color: var(--cas-theme-osf-red, #b52b27); - color: white; - margin: 0.25rem 0; +.login-section .reveal-password { + background-color: var(--cas-theme-osf-blue, #1b6d85); } -.banner-dismissible .close { - padding: 0rem 0.75rem; +.login-section .login-error-list { + margin-bottom: -0.5rem; } -.banner-danger .title-danger { +.login-section .card-message { + padding: 1rem 0; +} + +.login-instn-card .card-message, +.login-error-card .card-message { + padding: 0; +} + +.login-section .instn-select select { + width: 100%; + height: 56px; + font-size: 1.125rem; margin: 0.5rem 0; +} + +.login-error-list .banner { + border: none; + position: relative; +} + +.login-error-list .banner-dismissible { + background-color: transparent; color: var(--cas-theme-osf-red, #b52b27); - white-space: nowrap; + margin: -0.5rem 0; } -.banner-generic hr, -.banner-danger hr { - width: 100%; +.login-error-list .login-error-inline { + margin: 0.25rem 0; } -.card-message { - padding: 1rem 0; +.cas-field-primary { + margin-bottom: 1rem!important; + margin-top: 0.5rem!important; } -.w-card-narrow .card-message { - max-width: 36rem; +.cas-field-col-2 { + margin-top: 1rem!important; + margin-bottom: 1.5rem!important; } -.w-card-wide .card-message { - max-width: 48rem; +.cas-field-float-left { + float: left; } -@media all and (min-width: 639.99px) { - .w-card-narrow { - width: 50%; - } - .w-card-wide { - width: 75%; - } +.cas-field-float-right { + float: right; } -@media all and (min-width: 479.99px) and (max-width: 639.99px) { - .w-card-narrow { - width: 100%; - } - .w-card-wide { - width: 100%; - } - footer { - display: none - } +.cas-footer-osf { + text-align: center; + background: var(--cas-theme-osf-footer, #efefef); } -@media all and (min-height: 1439.99) { +@media all and (min-height: 1199.99px) { + .mdc-top-app-bar__row { height: 64px; } - .cas-brand { - height: 56px; + .mdc-top-app-bar__row .mdc-button { + height: 44px; + font-size: 1rem + } + .mdc-top-app-bar__row .cas-brand { + height: 40px; + } + .mdc-top-app-bar__row .cas-brand-name { + font-size: 2rem; } .mdc-top-app-bar--fixed-adjust { - padding-top: 72px; + padding-top: 64px; + } + + .mdc-top-app-bar__row .mdc-button { } - .mdc-drawer { - top: 56px; + + .service-ui { + margin-top: 1rem!important; + margin-bottom: 1rem!important; } + .cas-footer-osf { - font-size: 1rem; - padding-bottom: 1.5rem!important; - padding-top: 1.5rem!important; - text-align: center; + font-size: 1.125rem!important; + padding-bottom: 1.125rem!important; + padding-top: 1.125rem!important; } } -@media all and (min-width: 639.99px), -all and (min-height: 1079.99) { +@media all and (max-height: 1199.99px) { + .mdc-top-app-bar__row { height: 56px; } - .cas-brand { - height: 48px; + .mdc-top-app-bar__row .mdc-button { + height: 40px; + font-size: 1rem + } + .mdc-top-app-bar__row .cas-brand { + height: 36px; + } + .mdc-top-app-bar__row .cas-brand-name { + font-size: 1.75rem; } .mdc-top-app-bar--fixed-adjust { - padding-top: 64px; + padding-top: 48px; } - .mdc-drawer { - top: 48px; + + .service-ui { + margin-top: 0.875rem!important; + margin-bottom: 0.875rem!important; } + .cas-footer-osf { - font-size: 0.875rem; + font-size: 1rem!important; padding-bottom: 1rem!important; padding-top: 1rem!important; - text-align: center; } } -@media all and (min-width: 479.99px) and (max-width: 639.99px), -all and (max-height: 1079.99px) { +@media all and (max-height: 899.99px) { + .mdc-top-app-bar__row { - height: 40px; + height: 48px; + } + .mdc-top-app-bar__row .mdc-button { + height: 32px; + font-size: 0.875rem } - .cas-brand { + .mdc-top-app-bar__row .cas-brand { height: 32px; } - .mdc-drawer { - top: 32px; + .mdc-top-app-bar__row .cas-brand-name { + font-size: 1.5rem; } .mdc-top-app-bar--fixed-adjust { - padding-top: 32px; + padding-top: 36px; } + + .service-ui { + margin-top: 0.75rem!important; + margin-bottom: 0.75rem!important; + } + .cas-footer-osf { - font-size: 0.75rem; - padding-bottom: 0.75rem!important; - padding-top: 0.75rem!important; - text-align: center; + font-size: 0.875rem!important; + padding-bottom: 0.875rem!important; + padding-top: 0.875rem!important; } } -@media all and (max-width: 479.99px), -all and (max-height: 899.99px) { +@media all and (max-height: 699.99px) { + + .mdc-top-app-bar__row { + height: 40px; + } + .mdc-top-app-bar__row .cas-brand { + height: 28px; + } + .mdc-top-app-bar__row .cas-brand-name { + font-size: 1.25rem; + } + .mdc-top-app-bar__row .mdc-button { + height: 28px; + font-size: 0.75rem; + } + .mdc-top-app-bar--fixed-adjust { + padding-top: 24px; + } + .service-ui { margin-top: 0.5rem!important; margin-bottom: 0.5rem!important; } + + .cas-footer-osf { + font-size: 0.625rem!important; + padding-bottom: 0.625rem!important; + padding-top: 0.625rem!important; + } +} + +@media all and (min-width: 699.99px) { + + .w-card-narrow { + width: 50%; + } + .w-card-wide { + width: 75%; + } + .service-ui .service-ui-logo { - max-width: 240px; - max-height: 3rem; + max-width: 320px; } - .service-ui-info .service-ui-name, - .service-ui-info .service-ui-desc { - display: none; +} + +@media all and (max-width: 699.99px) { + + .mdc-top-app-bar__row .hidden-narrow, + .service-ui-name .osf-shield-with-name .hidden-narrow { + display: None; } - .login-section .mdi-before-text { - display: none; + + .w-card-narrow { + width: 100%; } - .login-section .text-with-mdi { - font-size: 0.75rem; + .w-card-wide { + width: 100%; } - .login-section .text-without-mdi { - font-size: 0.75rem; + + .service-ui .service-ui-logo { + max-width: 280px; } - .login-error-card .title-danger { - font-size: 0.875rem; + + .osf-shield-with-name .service-ui-logo { + max-height: 48px; } - .mdc-top-app-bar__row { - height: 32px; + + .osf-shield-with-name .service-ui-name { + font-size: 2rem; } - .cas-brand { - height: 24px; + + .osf-shield-with-name .service-ui-name-branded { + font-size: 1.75rem; } - .mdc-drawer { - top: 32px; +} + +@media all and (max-width: 511.99px) { + + .login-section, + .login-error-card { + width: fit-content; + min-width: 360px; } - .mdc-top-app-bar--fixed-adjust { - padding-top: 16px; + + .form-button-inline .mdc-button { + flex-basis: 100%; + min-width: fit-content; + font-size: 1rem; + padding: 0 16px; + } + + .form-button-inline .delegation-button-logo { + position: absolute; + left: 20px; + top: 11px; + padding: 2px; + height: 36px; + } + + .form-button-inline .delegation-button-label { + font-size: 1rem; + } + + .osf-shield-with-name .service-ui-name-branded { + white-space: normal; } +} + +@media all and (max-width: 399.99px) { + + .service-ui .service-ui-logo { + max-width: 240px; + } + + .osf-shield-with-name .service-ui-logo { + max-height: 36px; + } + + .osf-shield-with-name .service-ui-name { + font-size: 1.75rem; + } + + .osf-shield-with-name .service-ui-name-branded { + font-size: 1.5rem; + } + + .login-section .mdi-before-text, + .login-error-card .mdi-before-text { + display: none; + } + .cas-footer-osf { - font-size: 0.625rem; - padding-bottom: 0.5rem!important; - padding-top: 0.5rem!important; - text-align: center; + font-size: 0.625rem!important; + padding-bottom: 0.625rem!important; + padding-top: 0.625rem!important; } } + +/**************************************** + * End of customized styles for OSF CAS * + ****************************************/ diff --git a/src/main/resources/static/images/branded/preprints-africarxiv-logo.png b/src/main/resources/static/images/branded/preprints-africarxiv-logo.png new file mode 100644 index 0000000..667c18f Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-africarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-agrixiv-logo.png b/src/main/resources/static/images/branded/preprints-agrixiv-logo.png new file mode 100644 index 0000000..f453809 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-agrixiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-arabixiv-logo.png b/src/main/resources/static/images/branded/preprints-arabixiv-logo.png new file mode 100644 index 0000000..9e01641 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-arabixiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-banglarxiv-logo.png b/src/main/resources/static/images/branded/preprints-banglarxiv-logo.png new file mode 100644 index 0000000..b73d3cb Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-banglarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-biohackrxiv-logo.png b/src/main/resources/static/images/branded/preprints-biohackrxiv-logo.png new file mode 100644 index 0000000..6788509 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-biohackrxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-bitss-logo.png b/src/main/resources/static/images/branded/preprints-bitss-logo.png new file mode 100644 index 0000000..2653205 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-bitss-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-bodoarxiv-logo.png b/src/main/resources/static/images/branded/preprints-bodoarxiv-logo.png new file mode 100644 index 0000000..1fc93a0 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-bodoarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-casarxiv-logo.png b/src/main/resources/static/images/branded/preprints-casarxiv-logo.png new file mode 100644 index 0000000..4d06d99 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-casarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-eartharxiv-logo.png b/src/main/resources/static/images/branded/preprints-eartharxiv-logo.png new file mode 100644 index 0000000..78add22 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-eartharxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-ecoevorxiv-logo.png b/src/main/resources/static/images/branded/preprints-ecoevorxiv-logo.png new file mode 100644 index 0000000..98098b6 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-ecoevorxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-ecsarxiv-logo.png b/src/main/resources/static/images/branded/preprints-ecsarxiv-logo.png new file mode 100644 index 0000000..248db7e Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-ecsarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-edarxiv-logo.png b/src/main/resources/static/images/branded/preprints-edarxiv-logo.png new file mode 100644 index 0000000..66807d3 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-edarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-engrxiv-logo.png b/src/main/resources/static/images/branded/preprints-engrxiv-logo.png new file mode 100644 index 0000000..a5f5008 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-engrxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-experimentalprotocols-logo.png b/src/main/resources/static/images/branded/preprints-experimentalprotocols-logo.png new file mode 100644 index 0000000..036ffee Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-experimentalprotocols-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-focusarchive-logo.png b/src/main/resources/static/images/branded/preprints-focusarchive-logo.png new file mode 100644 index 0000000..68f8b39 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-focusarchive-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-frenxiv-logo.png b/src/main/resources/static/images/branded/preprints-frenxiv-logo.png new file mode 100644 index 0000000..b9657a7 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-frenxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-hsrxiv-logo.png b/src/main/resources/static/images/branded/preprints-hsrxiv-logo.png new file mode 100644 index 0000000..9b63e70 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-hsrxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-inarxiv-logo.png b/src/main/resources/static/images/branded/preprints-inarxiv-logo.png new file mode 100644 index 0000000..4c7e26c Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-inarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-indiarxiv-logo.png b/src/main/resources/static/images/branded/preprints-indiarxiv-logo.png new file mode 100644 index 0000000..ee8aeb4 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-indiarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-lawarxiv-logo.png b/src/main/resources/static/images/branded/preprints-lawarxiv-logo.png new file mode 100644 index 0000000..f241544 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-lawarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-lissa-logo.png b/src/main/resources/static/images/branded/preprints-lissa-logo.png new file mode 100644 index 0000000..860815b Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-lissa-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-livedata-logo.png b/src/main/resources/static/images/branded/preprints-livedata-logo.png new file mode 100644 index 0000000..2558d51 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-livedata-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-marxiv-logo.png b/src/main/resources/static/images/branded/preprints-marxiv-logo.png new file mode 100644 index 0000000..668f0a2 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-marxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-medarxiv-logo.png b/src/main/resources/static/images/branded/preprints-medarxiv-logo.png new file mode 100644 index 0000000..b88a13d Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-medarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-mediarxiv-logo.png b/src/main/resources/static/images/branded/preprints-mediarxiv-logo.png new file mode 100644 index 0000000..6787976 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-mediarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-metaarxiv-logo.png b/src/main/resources/static/images/branded/preprints-metaarxiv-logo.png new file mode 100644 index 0000000..b5d526c Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-metaarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-mindrxiv-logo.png b/src/main/resources/static/images/branded/preprints-mindrxiv-logo.png new file mode 100644 index 0000000..0a16891 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-mindrxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-nutrixiv-logo.png b/src/main/resources/static/images/branded/preprints-nutrixiv-logo.png new file mode 100644 index 0000000..94f0fe6 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-nutrixiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-paleorxiv-logo.png b/src/main/resources/static/images/branded/preprints-paleorxiv-logo.png new file mode 100644 index 0000000..0a43fd9 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-paleorxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-psyarxiv-logo.png b/src/main/resources/static/images/branded/preprints-psyarxiv-logo.png new file mode 100644 index 0000000..cd34b46 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-psyarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-scielo-logo.png b/src/main/resources/static/images/branded/preprints-scielo-logo.png new file mode 100644 index 0000000..46e1bbe Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-scielo-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-socarxiv-logo.png b/src/main/resources/static/images/branded/preprints-socarxiv-logo.png new file mode 100644 index 0000000..6b22136 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-socarxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-sportrxiv-logo.png b/src/main/resources/static/images/branded/preprints-sportrxiv-logo.png new file mode 100644 index 0000000..829e294 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-sportrxiv-logo.png differ diff --git a/src/main/resources/static/images/branded/preprints-thesiscommons-logo.png b/src/main/resources/static/images/branded/preprints-thesiscommons-logo.png new file mode 100644 index 0000000..b8f2955 Binary files /dev/null and b/src/main/resources/static/images/branded/preprints-thesiscommons-logo.png differ diff --git a/src/main/resources/static/images/institution-logo.png b/src/main/resources/static/images/institution-logo.png index a32a180..b2714fe 100644 Binary files a/src/main/resources/static/images/institution-logo.png and b/src/main/resources/static/images/institution-logo.png differ diff --git a/src/main/resources/static/images/osf-preprints-banner.png b/src/main/resources/static/images/osf-preprints-banner.png index b56823b..b00f14f 100644 Binary files a/src/main/resources/static/images/osf-preprints-banner.png and b/src/main/resources/static/images/osf-preprints-banner.png differ diff --git a/src/main/resources/static/images/osf-registries-banner.png b/src/main/resources/static/images/osf-registries-banner.png index f617af1..dbb5edd 100644 Binary files a/src/main/resources/static/images/osf-registries-banner.png and b/src/main/resources/static/images/osf-registries-banner.png differ diff --git a/src/main/resources/templates/casAccountDisabledView.html b/src/main/resources/templates/casAccountDisabledView.html index 2915fe3..fe38586 100644 --- a/src/main/resources/templates/casAccountDisabledView.html +++ b/src/main/resources/templates/casAccountDisabledView.html @@ -5,33 +5,46 @@ - + - -
-
+ + + + diff --git a/src/main/resources/templates/casAccountNotConfirmedIdPView.html b/src/main/resources/templates/casAccountNotConfirmedIdPView.html index 29fb0fe..6fcc69b 100644 --- a/src/main/resources/templates/casAccountNotConfirmedIdPView.html +++ b/src/main/resources/templates/casAccountNotConfirmedIdPView.html @@ -5,33 +5,46 @@ - + - -
-
+ + + + diff --git a/src/main/resources/templates/casAccountNotConfirmedOsfView.html b/src/main/resources/templates/casAccountNotConfirmedOsfView.html index de79a7b..d08f07d 100644 --- a/src/main/resources/templates/casAccountNotConfirmedOsfView.html +++ b/src/main/resources/templates/casAccountNotConfirmedOsfView.html @@ -5,38 +5,46 @@ - + - -
-
+ + + + diff --git a/src/main/resources/templates/casAutoRedirectToDefaultServiceLoginView.html b/src/main/resources/templates/casAutoRedirectToDefaultServiceLoginView.html new file mode 100644 index 0000000..292107d --- /dev/null +++ b/src/main/resources/templates/casAutoRedirectToDefaultServiceLoginView.html @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/src/main/resources/templates/casAutoRedirectToOrcidLoginView.html b/src/main/resources/templates/casAutoRedirectToOrcidLoginView.html index e8b286f..07eb39f 100644 --- a/src/main/resources/templates/casAutoRedirectToOrcidLoginView.html +++ b/src/main/resources/templates/casAutoRedirectToOrcidLoginView.html @@ -2,8 +2,8 @@ + and #{screen.orcidredirect.heading.} are gauranteed to exist. --> - + diff --git a/src/main/resources/templates/casGenericSuccessView.html b/src/main/resources/templates/casGenericSuccessView.html index af9e40b..47a4cbe 100644 --- a/src/main/resources/templates/casGenericSuccessView.html +++ b/src/main/resources/templates/casGenericSuccessView.html @@ -2,96 +2,112 @@ - - + + - - + + - -
-
- -
+ + + + + + diff --git a/src/main/resources/templates/casInstitutionLoginView.html b/src/main/resources/templates/casInstitutionLoginView.html new file mode 100644 index 0000000..a7659a5 --- /dev/null +++ b/src/main/resources/templates/casInstitutionLoginView.html @@ -0,0 +1,139 @@ + + + + + + + + + + + + +
+
+ +
+ + + + + +
+ + + + diff --git a/src/main/resources/templates/casInstitutionSsoFailedView.html b/src/main/resources/templates/casInstitutionSsoFailedView.html new file mode 100644 index 0000000..301e316 --- /dev/null +++ b/src/main/resources/templates/casInstitutionSsoFailedView.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + + diff --git a/src/main/resources/templates/casInstitutionSsoNotImplementedView.html b/src/main/resources/templates/casInstitutionSsoNotImplementedView.html deleted file mode 100644 index fa52971..0000000 --- a/src/main/resources/templates/casInstitutionSsoNotImplementedView.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - -
- -
- - - diff --git a/src/main/resources/templates/casInvalidUserStatusView.html b/src/main/resources/templates/casInvalidUserStatusView.html index 9cafce7..b49c518 100644 --- a/src/main/resources/templates/casInvalidUserStatusView.html +++ b/src/main/resources/templates/casInvalidUserStatusView.html @@ -5,33 +5,46 @@ - + - -
-
+ + + + diff --git a/src/main/resources/templates/casInvalidVerificationKeyView.html b/src/main/resources/templates/casInvalidVerificationKeyView.html index 8b8b16b..2f17532 100644 --- a/src/main/resources/templates/casInvalidVerificationKeyView.html +++ b/src/main/resources/templates/casInvalidVerificationKeyView.html @@ -5,33 +5,46 @@ - + - -
-
+ + + + diff --git a/src/main/resources/templates/casLoginView.html b/src/main/resources/templates/casLoginView.html index a82b3c6..db87cd5 100644 --- a/src/main/resources/templates/casLoginView.html +++ b/src/main/resources/templates/casLoginView.html @@ -5,7 +5,7 @@ - + @@ -13,8 +13,7 @@
-
+
+
+ +
+
+
+

+
+
+
+ + + +
+
+
+ + +
+
- + + +
diff --git a/src/main/resources/templates/casServiceErrorView.html b/src/main/resources/templates/casServiceErrorView.html index e7e173d..a937152 100644 --- a/src/main/resources/templates/casServiceErrorView.html +++ b/src/main/resources/templates/casServiceErrorView.html @@ -2,36 +2,49 @@ - - + + - Service Error View + - -
-
+ + + +
diff --git a/src/main/resources/templates/casTermsOfServiceConsentView.html b/src/main/resources/templates/casTermsOfServiceConsentView.html new file mode 100644 index 0000000..0dd7163 --- /dev/null +++ b/src/main/resources/templates/casTermsOfServiceConsentView.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + +
+ +
+ +
+
+ + + diff --git a/src/main/resources/templates/casTwoFactorLoginView.html b/src/main/resources/templates/casTwoFactorLoginView.html index a3a5d43..c630bf1 100644 --- a/src/main/resources/templates/casTwoFactorLoginView.html +++ b/src/main/resources/templates/casTwoFactorLoginView.html @@ -5,16 +5,14 @@ - + -
-
+
+
+
+ + +
+
+ +
+
+ +
+
+

+                        

+                        

+                    
+
+ + +
+
+
+

+                    
+
+ +
- + -
- + + +
diff --git a/src/main/resources/templates/error/401.html b/src/main/resources/templates/error/401.html new file mode 100644 index 0000000..874b937 --- /dev/null +++ b/src/main/resources/templates/error/401.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + +
+
+ +
+
+ + + diff --git a/src/main/resources/templates/error/403.html b/src/main/resources/templates/error/403.html new file mode 100644 index 0000000..b8a7a85 --- /dev/null +++ b/src/main/resources/templates/error/403.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + +
+
+ +
+
+ + + diff --git a/src/main/resources/templates/error/404.html b/src/main/resources/templates/error/404.html new file mode 100644 index 0000000..42b2814 --- /dev/null +++ b/src/main/resources/templates/error/404.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + +
+
+ +
+
+ + + diff --git a/src/main/resources/templates/error/405.html b/src/main/resources/templates/error/405.html new file mode 100644 index 0000000..678257e --- /dev/null +++ b/src/main/resources/templates/error/405.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + +
+
+ +
+
+ + + diff --git a/src/main/resources/templates/error/423.html b/src/main/resources/templates/error/423.html new file mode 100644 index 0000000..1666cf2 --- /dev/null +++ b/src/main/resources/templates/error/423.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + +
+
+ +
+
+ + + diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index 963b6f5..cb393f9 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -14,94 +14,49 @@
- + + + + + diff --git a/src/main/resources/templates/fragments/loginform.html b/src/main/resources/templates/fragments/loginform.html index f1d3a71..3fdc20f 100644 --- a/src/main/resources/templates/fragments/loginform.html +++ b/src/main/resources/templates/fragments/loginform.html @@ -12,59 +12,43 @@
+
-
+ +
-
- -

- - -

- -
-
- - - - -
-
- -
-
- - - - -
-
- -
+ + +
+ +
+ +
+ + + + + + + + +
+ +

-
+
-
-
-
- -
-
+
- +
  - -

+
@@ -147,23 +133,33 @@

-
- - +
+
+
-
+
+ +   + + + + +
+ +
+
-
+ + + + +
+
+
+ + + diff --git a/src/main/resources/templates/fragments/totploginform.html b/src/main/resources/templates/fragments/totploginform.html index f044a79..0b1c5f1 100644 --- a/src/main/resources/templates/fragments/totploginform.html +++ b/src/main/resources/templates/fragments/totploginform.html @@ -12,36 +12,28 @@
+
-
+
-
+ -

- - -

+
+ +
-
-
-
- -
-
+
- +
readonly th:disabled="${@casThymeleafLoginFormDirector.isLoginFormUsernameInputDisabled(#vars)}" th:field="*{username}" - th:accesskey="#{screen.welcome.label.email.accesskey}" + th:accesskey="#{screen.twofactor.label.email.accesskey}" th:value="${@casThymeleafLoginFormDirector.getLoginFormUsername(#vars)}" autocomplete="off" /> - +
@@ -92,12 +84,20 @@

- + +
@@ -111,31 +111,37 @@

-
- -
- - - -
+
+
+

-
-

-
+
+

+
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+
+
+
+
+
+ +
+ +
+ + +