From 9b1f69442003289aa2f92686c49d6e98e4e0e2c0 Mon Sep 17 00:00:00 2001 From: Arnab Dutta Date: Wed, 8 Nov 2023 13:10:48 +0530 Subject: [PATCH] feat(jans-tarp): user should be allowed to paste an SSA (or specify a file from disk) in DCR form #6161 (#6467) * feat: user should be allowed to paste an SSA (or specify a file from disk) in DCR form #6161 Signed-off-by: Arnab Dutta * feat: user should be allowed to paste an SSA (or specify a file from disk) in DCR form #6161 Signed-off-by: Arnab Dutta * feat: user should be allowed to paste an SSA (or specify a file from disk) in DCR form #6161 Signed-off-by: Arnab Dutta --------- Signed-off-by: Arnab Dutta --- demos/jans-tarp/package.json | 3 + .../jans-tarp/src/options/StyledDropzone.tsx | 77 ++++ demos/jans-tarp/src/options/options.css | 4 +- demos/jans-tarp/src/options/registerForm.tsx | 361 +++++++++++------- 4 files changed, 315 insertions(+), 130 deletions(-) create mode 100644 demos/jans-tarp/src/options/StyledDropzone.tsx diff --git a/demos/jans-tarp/package.json b/demos/jans-tarp/package.json index da670f216a2..f12daf3101b 100644 --- a/demos/jans-tarp/package.json +++ b/demos/jans-tarp/package.json @@ -32,13 +32,16 @@ "adm-zip": "^0.5.10", "autoprefixer": "^10.4.7", "axios": "^1.4.0", + "jwt-decode": "^4.0.0", "moment": "^2.29.4", "postcss": "^8.4.14", "qs": "^6.11.2", "react-datepicker": "^4.12.0", + "react-dropzone": "^14.2.3", "react-router-dom": "6.2", "react-select": "^5.7.3", "react-spinner-overlay": "^0.1.33", + "styled-components": "^6.1.0", "uuid": "^9.0.0" } } diff --git a/demos/jans-tarp/src/options/StyledDropzone.tsx b/demos/jans-tarp/src/options/StyledDropzone.tsx new file mode 100644 index 00000000000..03f586d62b2 --- /dev/null +++ b/demos/jans-tarp/src/options/StyledDropzone.tsx @@ -0,0 +1,77 @@ +import React, { useEffect, useCallback, useState } from 'react'; +import { useDropzone } from 'react-dropzone'; +import styled from 'styled-components'; + +const getColor = (props) => { + if (props.isDragAccept) { + return '#00e676'; + } + if (props.isDragReject) { + return '#ff1744'; + } + if (props.isFocused) { + return '#2196f3'; + } + return '#eeeeee'; +} + +const Container = styled.div` + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + border-width: 2px; + border-radius: 2px; + border-color: ${props => getColor(props)}; + border-style: dashed; + background-color: #fafafa; + color: #bdbdbd; + outline: none; + transition: border .24s ease-in-out; + margin-bottom: 30px; +`; + +function StyledDropzone(props) { + + const [selectedFileName, setSelectedFileName] = useState(null) + const [selectedFile, setSelectedFile] = useState(null) + + useEffect(() => { + if (selectedFile) { + props.submitFile(selectedFile) + } + }, [selectedFile]) + + const onDrop = useCallback(acceptedFiles => { + const file = acceptedFiles[0] + setSelectedFileName(file.name) + setSelectedFile(file) + }, []) + const { getRootProps, + getInputProps, + isFocused, + isDragAccept, + isDragReject } = useDropzone({ + onDrop, + accept: { + 'application/jwt': ['.jwt'], + }, + }) + + + return ( +
+ + + {selectedFileName ? ( + Selected File : {selectedFileName} + ) : ( +

Drag 'n' drop .jwt file here, or click to select file

+ )} +
+
+ ); +} + +export default StyledDropzone; \ No newline at end of file diff --git a/demos/jans-tarp/src/options/options.css b/demos/jans-tarp/src/options/options.css index 16e5d49877a..62363440e42 100644 --- a/demos/jans-tarp/src/options/options.css +++ b/demos/jans-tarp/src/options/options.css @@ -95,7 +95,7 @@ fieldset { legend { font-size: 1.4em; - margin-bottom: 10px; + margin-bottom: 20px; } label { @@ -142,7 +142,7 @@ label.light { display: flex; flex-direction: column; justify-content: center; - align-items: center; + align-items: left; min-width: 600px; font-size: 15px; padding: 2rem; diff --git a/demos/jans-tarp/src/options/registerForm.tsx b/demos/jans-tarp/src/options/registerForm.tsx index b41df3f6d88..d743f15d501 100644 --- a/demos/jans-tarp/src/options/registerForm.tsx +++ b/demos/jans-tarp/src/options/registerForm.tsx @@ -1,4 +1,4 @@ -import React, { useState, KeyboardEventHandler } from 'react' +import React, { useState, KeyboardEventHandler, useCallback, useEffect } from 'react' import axios from 'axios'; import './options.css' import { v4 as uuidv4 } from 'uuid'; @@ -9,6 +9,8 @@ import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import moment from 'moment'; import { ILooseObject } from './ILooseObject'; +import StyledDropzone from './StyledDropzone'; +import { jwtDecode } from "jwt-decode"; const components = { DropdownIndicator: null, }; @@ -24,10 +26,178 @@ const RegisterForm = (data) => { const [inputValueIssuer, setInputValueIssuer] = useState(''); const [inputValueScope, setInputValueScope] = useState(''); const [isLoading, setIsLoading] = useState(false); + const [uploadSsa, setUploadSsa] = useState(false); const [showClientExpiry, setShowClientExpiry] = useState(false); const [issuerOption, setIssuerOption] = useState([]); const [scopeOption, setScopeOption] = useState([createOption('openid')]); const [clientExpiryDate, setClientExpiryDate] = useState(moment().add(1, 'days').toDate()); + const [ssaJwt, setSsaJwt] = useState(null) + const REGISTRATION_ERROR = 'Error in registration. Check web console for logs.' + + const readJWTFile = (selectedFile) => { + const reader = new FileReader() + + reader.onload = () => { + const token = reader.result + setSsaJwt(token) + } + + const blob = new Blob([selectedFile]) + reader.readAsText(blob) + } + + async function triggerClientRegistration() { + try { + if (uploadSsa) { + const response = await processForClientRegiWithSSA() + if (response.result !== 'success') { + setError(REGISTRATION_ERROR); + } + setPageLoading(false); + } else if (validate()) { + + setPageLoading(true); + const response = await processForClientRegiWithoutSSA() + if (response.result !== 'success') { + setError(REGISTRATION_ERROR); + } + setPageLoading(false); + } + } catch (err) { + console.error(err) + } + } + + async function processForClientRegiWithSSA() { + try { + const ssaJson = jwtDecode(ssaJwt) + if (ssaJson.iss == null) { + setError('Issuer not found in SSA.') + } + + const issuer = ssaJson.iss + + const configurationUrl = generateOpenIdConfigurationURL(issuer) + const openapiConfig = await getOpenidConfiguration(configurationUrl) + + if (openapiConfig != undefined) { + chrome.storage.local.set({ opConfiguration: openapiConfig.data }).then(() => { + console.log("openapiConfig is set to " + openapiConfig); + }); + + const registrationUrl = openapiConfig.data.registration_endpoint; + + var registerObj: ILooseObject = { + redirect_uris: [issuer], + software_statement: ssaJwt, + response_types: ['code'], + grant_types: ['authorization_code'], + application_type: 'web', + client_name: 'jans-tarp-' + uuidv4(), + token_endpoint_auth_method: 'client_secret_basic', + post_logout_redirect_uris: [chrome.runtime.getURL('options.html')] + } + + const registrationResp = await registerOIDCClient(registrationUrl, registerObj) + + if (registrationResp !== undefined) { + chrome.storage.local.set({ + oidcClient: { + 'op_host': issuer, + 'client_id': registrationResp.data.client_id, + 'client_secret': registrationResp.data.client_secret, + 'scope': registerObj.scope, + 'redirect_uri': registerObj.redirect_uris, + 'authorization_endpoint': openapiConfig.data.authorization_endpoint, + 'response_type': registerObj.response_types, + 'post_logout_redirect_uris': registerObj.post_logout_redirect_uris, + 'expire_at': clientExpiryDate.getTime(), + 'showClientExpiry': showClientExpiry + + } + }) + console.log('OIDC client registered successfully!') + console.log("oidcClient is set for client_id: " + registrationResp.data.client_id) + + return await { result: "success", message: "Regstration successful!" } + + } else { + return await { result: "error", message: REGISTRATION_ERROR } + } + } else { + return await { result: "error", message: "Error in fetching Openid configuration!" } + } + } catch (err) { + console.error(err) + return { result: "error", message: REGISTRATION_ERROR + err.message } + } + } + + async function processForClientRegiWithoutSSA() { + try { + const opConfigurationEndpoint = generateOpenIdConfigurationURL(issuerOption.map((iss) => iss.value)[0]); + const opConfigurationEndpointURL = new URL(opConfigurationEndpoint); + const issuer = opConfigurationEndpointURL.protocol + '//' + opConfigurationEndpointURL.hostname; + const scope = scopeOption.map((ele) => ele.value).join(" "); + + const openapiConfig = await getOpenidConfiguration(opConfigurationEndpoint); + + if (openapiConfig != undefined) { + chrome.storage.local.set({ opConfiguration: openapiConfig.data }).then(() => { + console.log("openapiConfig is set to " + openapiConfig); + }); + + const registrationUrl = openapiConfig.data.registration_endpoint; + + var registerObj: ILooseObject = { + redirect_uris: [issuer], + scope: scope, + post_logout_redirect_uris: [chrome.runtime.getURL('options.html')], + response_types: ['code'], + grant_types: ['authorization_code'], + application_type: 'web', + client_name: 'jans-tarp-' + uuidv4(), + token_endpoint_auth_method: 'client_secret_basic' + }; + + if (showClientExpiry) { + registerObj.lifetime = ((clientExpiryDate.getTime() - moment().toDate().getTime()) / 1000); + } + + const registrationResp = await registerOIDCClient(registrationUrl, registerObj); + + if (registrationResp !== undefined) { + chrome.storage.local.set({ + oidcClient: { + 'op_host': issuer, + 'client_id': registrationResp.data.client_id, + 'client_secret': registrationResp.data.client_secret, + 'scope': registerObj.scope, + 'redirect_uri': registerObj.redirect_uris, + 'authorization_endpoint': openapiConfig.data.authorization_endpoint, + 'response_type': registerObj.response_types, + 'post_logout_redirect_uris': registerObj.post_logout_redirect_uris, + 'expire_at': clientExpiryDate.getTime(), + 'showClientExpiry': showClientExpiry + + } + }) + console.log('OIDC client registered successfully!') + console.log("oidcClient is set for client_id: " + registrationResp.data.client_id); + + return await { result: "success", message: "Regstration successful!" }; + + } else { + return await { result: "error", message: REGISTRATION_ERROR }; + } + } else { + return await { result: "error", message: "Error in fetching Openid configuration!" }; + } + } catch (err) { + console.error(err) + return { result: "error", message: REGISTRATION_ERROR }; + } + } const handleKeyDown: KeyboardEventHandler = async (event) => { const inputId = (event.target as HTMLInputElement).id; @@ -114,86 +284,6 @@ const RegisterForm = (data) => { return true; } - async function registerClient() { - if (validate()) { - try { - setPageLoading(true); - const response = await register() - if (response.result !== 'success') { - setError('Error in registration.'); - } - setPageLoading(false); - } catch (err) { - console.error(err) - } - } - } - - async function register() { - try { - const opConfigurationEndpoint = generateOpenIdConfigurationURL(issuerOption.map((iss) => iss.value)[0]); - const opConfigurationEndpointURL = new URL(opConfigurationEndpoint); - const issuer = opConfigurationEndpointURL.protocol + '//' + opConfigurationEndpointURL.hostname; - const scope = scopeOption.map((ele) => ele.value).join(" "); - const openapiConfig = await getOpenidConfiguration(opConfigurationEndpoint); - - if (openapiConfig != undefined) { - chrome.storage.local.set({ opConfiguration: openapiConfig.data }).then(() => { - console.log("openapiConfig is set to " + openapiConfig); - }); - - const registrationUrl = openapiConfig.data.registration_endpoint; - - var registerObj: ILooseObject = { - redirect_uris: [issuer], - scope: scope, - post_logout_redirect_uris: [chrome.runtime.getURL('options.html')], - response_types: ['code'], - grant_types: ['authorization_code', 'client_credentials'], - application_type: 'web', - client_name: 'Gluu-RP-' + uuidv4(), - token_endpoint_auth_method: 'client_secret_basic' - }; - - if (showClientExpiry) { - registerObj.lifetime = ((clientExpiryDate.getTime() - moment().toDate().getTime()) / 1000); - } - - const registrationResp = await registerOIDCClient(registrationUrl, registerObj); - - if (registrationResp !== undefined) { - chrome.storage.local.set({ - oidcClient: { - 'op_host': issuer, - 'client_id': registrationResp.data.client_id, - 'client_secret': registrationResp.data.client_secret, - 'scope': registerObj.scope, - 'redirect_uri': registerObj.redirect_uris, - 'authorization_endpoint': openapiConfig.data.authorization_endpoint, - 'response_type': registerObj.response_types, - 'post_logout_redirect_uris': registerObj.post_logout_redirect_uris, - 'expire_at': clientExpiryDate.getTime(), - 'showClientExpiry': showClientExpiry - - } - }) - console.log('OIDC client registered successfully!') - console.log("oidcClient is set for client_id: " + registrationResp.data.client_id); - - return await { result: "success", message: "Regstration successful!" }; - - } else { - return await { result: "error", message: "Error in registration!" }; - } - } else { - return await { result: "error", message: "Error in fetching Openid configuration!" }; - } - } catch (err) { - console.error(err) - return { result: "error", message: "Error in registration!" }; - } - } - async function registerOIDCClient(registration_endpoint, registerObj) { try { const registerReqOptions = { @@ -223,60 +313,75 @@ const RegisterForm = (data) => { } } + function handleChange(event) { + setSsaJwt(event.target.value) + } + return (
O Register OIDC Client {error.length > 0 ? {error} : ""} - - setIssuerOption(newValue)} - onInputChange={(newValue) => setInputValueIssuer(newValue)} - onKeyDown={handleKeyDown} - placeholder="Type something and press enter..." - value={issuerOption} - className="inputText" - /> - - - {showClientExpiry ? + + {uploadSsa ? <> - - setClientExpiryDate(date)} - minDate={new Date()} - className="inputText inputStyle" - dateFormat="yyyy/MM/dd h:mm aa" + + +