diff --git a/README.md b/README.md index f252488..b5ea3cf 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,18 @@ In addition, it expects the certificate and keyfiles under ```/certificates/cert ## Usage -The easiest way to use the tool is docker: +The easiest way to use the tool is docker. Provide a config-file and the corresponding certificate and key. The config file is of form: + +```yaml +EU.EORI.NLHAPPYPETS: + certificate: "/happypets/certificate.pem" + key: "/happypets/key.pem" +``` + +Execute the tool and provide the client and idp id to use from the config: ```shell - docker run -v $(pwd)/example:/certificates -e I_SHARE_CLIENT_ID="EU.EORI.NLHAPPYPETS" -e I_SHARE_IDP_ID="EU.EORI.NLPACKETDEL" quay.io/wi_stefan/ishare-jwt-helper + docker run -v $(pwd)/example:/happypets -v $(pwd)/example/config.yaml:/config.yaml -e I_SHARE_CLIENT_ID="EU.EORI.NLHAPPYPETS" -e I_SHARE_IDP_ID="EU.EORI.NLPACKETDEL" quay.io/wi_stefan/ishare-jwt-helper ``` This will create a token, using the test certificate and key in the [example-folder](./example/) for the client ```EU.EORI.NLHAPPYPETS``` and the intended participant ```EU.EORI.NLPACKETDEL```. @@ -29,4 +37,24 @@ Result: time="2022-06-22T07:01:49Z" level=info msg="CredentialsFolderPath: /certificates" time="2022-06-22T07:01:49Z" level=info msg="Token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlEOXpDQ0F0K2dBd0lCQWdJVVZVckZwOW5wZ1JGN1N5WTRlN05OWDU2aFl5NHdEUVlKS29aSWh2Y05BUUVMQlFBd2dZb3hDekFKQmdOVkJBWVRBa1JGTVE4d0RRWURWUVFJREFaQ1pYSnNhVzR4RHpBTkJnTlZCQWNNQmtKbGNteHBiakVmTUIwR0ExVUVDZ3dXUmtsWFFWSkZJRVp2ZFc1a1lYUnBiMjRnWlM1V0xqRU1NQW9HQTFVRUN3d0RSR1YyTVNvd0tBWUpLb1pJaHZjTkFRa0JGaHRtYVhkaGNtVXRkR1ZqYUMxb1pXeHdRR1pwZDJGeVpTNXZjbWN3SGhjTk1qRXhNVEkwTVRJeE5USXhXaGNOTWpFeE1qSTBNVEl4TlRJeFdqQ0JpakVMTUFrR0ExVUVCaE1DUkVVeER6QU5CZ05WQkFnTUJrSmxjbXhwYmpFUE1BMEdBMVVFQnd3R1FtVnliR2x1TVI4d0hRWURWUVFLREJaR1NWZEJVa1VnUm05MWJtUmhkR2x2YmlCbExsWXVNUXd3Q2dZRFZRUUxEQU5FWlhZeEtqQW9CZ2txaGtpRzl3MEJDUUVXRzJacGQyRnlaUzEwWldOb0xXaGxiSEJBWm1sM1lYSmxMbTl5WnpDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTFg5bWpYNjBjZDRUdVJzTVVCTWVZRVhMVXhsd2lXeEtreForcVNocW5ib1pZaFVlNEFMc1FSRDdvZ0xicjh1WXE4V2prMnFZM2M0WVZHRm4xaEdMZ2c2S2JkV0N6ZnVFbWFKN0pPTy9uQ3hkeGd0MkpvcXpkazhobFU4WUZaRlk0djNCMXZIb2h0TS9kTEU5VnNvTWNndWJPelArVmhkRXY1aExLUFJnR0FuS0IyaGhzN1ZXNERHeUM2QXBMZWRBU1Z3bzhob0NoTUM1cXFwRWhQWXlLdkpBWWJOV1Y5dndpLyswdXJ0cElNdkpocUNjR3ZHTi9TMUtiQlF5THFYakJBRnlSWFptMXBFYWFKTWxKTm00R1c2eUxLZUhhZFBlQndaUnpTR09kZFh4ZzFnaWlXcWtITGtZUUFnV0xXQVcyWEJFZ2VzTHlMa3FTNHNmVnhYb2NNQ0F3RUFBYU5UTUZFd0hRWURWUjBPQkJZRUZFNFF4SkU4WlhCWnM5V2NUcGlZZk1HK2psUjNNQjhHQTFVZEl3UVlNQmFBRkU0UXhKRThaWEJaczlXY1RwaVlmTUcramxSM01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFCbmhkQ0YyTi9SYVFRc0RGMGx6aGlFdkJDa010eWQyVnNOR0ZBcnlXVXl1akpSYlhCQ1hxNkMzdW1jQy9qSHJpd3VIc0JZSkZsSk04VGI4bmVhTTJRRlhRdGxFb0ozVDNuMndVSzArUk1zcUFlb2dRdFFsVVZZMU5ndktlb0pHdXBmQm9qRUxrQU9IWXFNZVhQT0NITjN3cEhZWkYwUWZOMXowYWlxV1FCZ3h1V3A3M2ErQUs3SDg2UEpJOGVleTNib0cwR3lEdG9QVndXNWJSZ0hlbzU5NUVGRHBTZWRJcmQ5ckhWY3F3RUdZRmpoNUZsYmJLQkVqTkRVVjRKbFp1L0pwTWErK0RuUERkOTBFRldaR0tzWnBpM0FMbXlhL2w5RHBFU1NNV1hXZTlaQ1M3cjZ2SGZMQldsNThvcmpYdUtySXRtZ3BZRXZHbVZPKzJ4WkJ1RWc9Il19.eyJhdWQiOiJFVS5FT1JJLk5MUEFDS0VUREVMIiwiZXhwIjoxNjU1ODgxMzM5LCJpYXQiOjE2NTU4ODEzMDksImlzcyI6IkVVLkVPUkkuTkxIQVBQWVBFVFMiLCJqdGkiOiI0MWU5OWE0OC05Mjc4LTRjMjYtOTM3ZC1iYTAxYTQ0OWFiNWUiLCJzdWIiOiJFVS5FT1JJLk5MSEFQUFlQRVRTIn0.ExdTEUp20_5a-M0yu4EQd0dZDhu4u5HCYCQQIb0JwSc6jUrSxgcTu2lTCDA5ct-M9oCBWBSOI2EPR-0DPzzQftZC-7YD_BmAsCnXOzUbR7nKSYrCzM7CwngwhriVLc_pVfzineyG90UsHmPlV1P9n785zEukNzuZLfNyqxtT_z1zfNLl0bg4dc9yz9euLv3zdvOXDsMOI21UPgu3qcGhr0rNStK7Og8AzodHdCZoDyctzKMjiGRIMQzAdmXFIqbx3QAjlQPN0pyG_-3OM8_I685BhCYXvT6ATw-D9HJmWlbyxADccs112S38_LVnOc_DoUBVeYZTFVQAjgwEmIXb9g" ``` -Copy the token and use it. Be aware that its only valid for 30s, due to [the specifcation of iShare](https://dev.ishareworks.org/introduction/jwt.html#jwt-payload). \ No newline at end of file +Copy the token and use it. Be aware that its only valid for 30s, due to [the specifcation of iShare](https://dev.ishareworks.org/introduction/jwt.html#jwt-payload). + +## Run as server + +When the environment variable ```RUN_SERVER``` is set to ```true```, the tool can run as a service. Tokens can be requested with the clientId(has to be configured in the config-file) and the idpId as the the audience of the token. ```/token?clientId=EU.EORI.NLHAPPYPETS&idpId=EU.EORI.NLPACKETDEL```. + +```shell + docker run -v $(pwd)/example:/happypets -v $(pwd)/example/config.yaml:/config.yaml -e RUN_SERVER="true" -p 8080:8080 quay.io/wi_stefan/ishare-jwt-helper +``` + +Get the token: + +```shell + curl --location --request GET 'localhost:8080/token?clientId=EU.EORI.NLHAPPYPETS&idpId=EU.EORI.NLPACKETDEL' \ +``` +Response: +```json +{ + "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlEOXpDQ0F0K2dBd0lCQWdJVVZVckZwOW5wZ1JGN1N5WTRlN05OWDU2aFl5NHdEUVlKS29aSWh2Y05BUUVMQlFBd2dZb3hDekFKQmdOVkJBWVRBa1JGTVE4d0RRWURWUVFJREFaQ1pYSnNhVzR4RHpBTkJnTlZCQWNNQmtKbGNteHBiakVmTUIwR0ExVUVDZ3dXUmtsWFFWSkZJRVp2ZFc1a1lYUnBiMjRnWlM1V0xqRU1NQW9HQTFVRUN3d0RSR1YyTVNvd0tBWUpLb1pJaHZjTkFRa0JGaHRtYVhkaGNtVXRkR1ZqYUMxb1pXeHdRR1pwZDJGeVpTNXZjbWN3SGhjTk1qRXhNVEkwTVRJeE5USXhXaGNOTWpFeE1qSTBNVEl4TlRJeFdqQ0JpakVMTUFrR0ExVUVCaE1DUkVVeER6QU5CZ05WQkFnTUJrSmxjbXhwYmpFUE1BMEdBMVVFQnd3R1FtVnliR2x1TVI4d0hRWURWUVFLREJaR1NWZEJVa1VnUm05MWJtUmhkR2x2YmlCbExsWXVNUXd3Q2dZRFZRUUxEQU5FWlhZeEtqQW9CZ2txaGtpRzl3MEJDUUVXRzJacGQyRnlaUzEwWldOb0xXaGxiSEJBWm1sM1lYSmxMbTl5WnpDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTFg5bWpYNjBjZDRUdVJzTVVCTWVZRVhMVXhsd2lXeEtreForcVNocW5ib1pZaFVlNEFMc1FSRDdvZ0xicjh1WXE4V2prMnFZM2M0WVZHRm4xaEdMZ2c2S2JkV0N6ZnVFbWFKN0pPTy9uQ3hkeGd0MkpvcXpkazhobFU4WUZaRlk0djNCMXZIb2h0TS9kTEU5VnNvTWNndWJPelArVmhkRXY1aExLUFJnR0FuS0IyaGhzN1ZXNERHeUM2QXBMZWRBU1Z3bzhob0NoTUM1cXFwRWhQWXlLdkpBWWJOV1Y5dndpLyswdXJ0cElNdkpocUNjR3ZHTi9TMUtiQlF5THFYakJBRnlSWFptMXBFYWFKTWxKTm00R1c2eUxLZUhhZFBlQndaUnpTR09kZFh4ZzFnaWlXcWtITGtZUUFnV0xXQVcyWEJFZ2VzTHlMa3FTNHNmVnhYb2NNQ0F3RUFBYU5UTUZFd0hRWURWUjBPQkJZRUZFNFF4SkU4WlhCWnM5V2NUcGlZZk1HK2psUjNNQjhHQTFVZEl3UVlNQmFBRkU0UXhKRThaWEJaczlXY1RwaVlmTUcramxSM01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFCbmhkQ0YyTi9SYVFRc0RGMGx6aGlFdkJDa010eWQyVnNOR0ZBcnlXVXl1akpSYlhCQ1hxNkMzdW1jQy9qSHJpd3VIc0JZSkZsSk04VGI4bmVhTTJRRlhRdGxFb0ozVDNuMndVSzArUk1zcUFlb2dRdFFsVVZZMU5ndktlb0pHdXBmQm9qRUxrQU9IWXFNZVhQT0NITjN3cEhZWkYwUWZOMXowYWlxV1FCZ3h1V3A3M2ErQUs3SDg2UEpJOGVleTNib0cwR3lEdG9QVndXNWJSZ0hlbzU5NUVGRHBTZWRJcmQ5ckhWY3F3RUdZRmpoNUZsYmJLQkVqTkRVVjRKbFp1L0pwTWErK0RuUERkOTBFRldaR0tzWnBpM0FMbXlhL2w5RHBFU1NNV1hXZTlaQ1M3cjZ2SGZMQldsNThvcmpYdUtySXRtZ3BZRXZHbVZPKzJ4WkJ1RWc9Il19.eyJhdWQiOiJFVS5FT1JJLk5MUEFDS0VUREVMIiwiZXhwIjoxNjY5MTAxMDk5LCJpYXQiOjE2NjkxMDEwNjksImlzcyI6IkVVLkVPUkkuTkxIQVBQWVBFVFMiLCJqdGkiOiJjODAzNWZlMi0xODI4LTQ4YzUtYTU3Yi04OGFiMTdmYTI5OGMiLCJzdWIiOiJFVS5FT1JJLk5MSEFQUFlQRVRTIn0.MbCdMRzoRPZNQrwtwQdFws5E40JWaCglG8ozblXwpUD2Wt3PWAshDEU7gkiTtoTkWSYnpmnfFo4a4fT9DbsWycM-xRR0BKH3pcIPawDVJsag9mk91Q9nGcYXjK54-kUx0nKrko0P7BUhjE5IVrjXtnQxLqGJo-_M7SfFsBxegDRiBu9qB8bTIBENNSMCq_gvcOjeGR0hlRvXlFz4vDxLMHRxadiY9NGfX3duNKKW1dx1vlHx4n0LJ2RwgafMIBLjYmeuSYMb64WoHDMaEff6Yg1H-c_eyGosjeEmLmIBW9ADCC8rMXz0ySq5StdKTIyo92sLrlj5oBREiuGGrHEy7A" +} +``` \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..07cf6a9 --- /dev/null +++ b/config.go @@ -0,0 +1,10 @@ +package main + +type Config struct { + Credentials map[string]Credential +} + +type Credential struct { + Certificate string `yaml:"certificate"` + Key string `yaml:"key"` +} diff --git a/example/config.yaml b/example/config.yaml new file mode 100644 index 0000000..c81b5e0 --- /dev/null +++ b/example/config.yaml @@ -0,0 +1,3 @@ +EU.EORI.NLHAPPYPETS: + certificate: "/happypets/certificate.pem" + key: "/happypets/key.pem" diff --git a/main.go b/main.go index 71ef1ad..1ca5881 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" + "errors" "fmt" "io/ioutil" "net/http" @@ -16,18 +17,18 @@ import ( "github.com/golang-jwt/jwt" "github.com/google/uuid" log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" ) var runServer bool = false -var iShareClientID string -var iShareIdpID string -var defaultKeyPath string = "/certificates/key.pem" -var defaultCertPath string = "/certificates/certificate.pem" -var keyPath string -var certificatePath string +var defaultConfigPath string = "/config.yaml" +var credentials map[string]Credential = map[string]Credential{} var serverPort int = 8080 +var defaultIShareClientID string +var defaultIShareIdpID string + func init() { serverEnabled, err := strconv.ParseBool(os.Getenv("RUN_SERVER")) @@ -41,24 +42,26 @@ func init() { log.Warnf("No valid server port was provided, run on default %s.", serverPort) } - iShareClientID = os.Getenv("I_SHARE_CLIENT_ID") - iShareIdpID = os.Getenv("I_SHARE_IDP_ID") - if iShareClientID == "" { - log.Fatalf("No I_SHARE_CLIENT_ID provided") - return + configPath := os.Getenv("CONFIG_PATH") + if configPath == "" { + configPath = defaultConfigPath } - if iShareIdpID == "" { - log.Fatalf("No I_SHARE_IDP_ID provided") - return - } - keyPath = os.Getenv("KEY_PATH") - certificatePath = os.Getenv("CERT_PATH") - if keyPath == "" { - keyPath = defaultKeyPath + + configBytes, err := readFile(configPath) + + if err != nil { + log.Fatalf("Was not able to read the config %s.", configPath, err) } - if certificatePath == "" { - certificatePath = defaultCertPath + + err = yaml.Unmarshal(configBytes, credentials) + if err != nil { + log.Fatalf("Was not able to unmarshal config %s.", configPath, err) } + + log.Infof("Config is: %v", credentials) + + defaultIShareClientID = os.Getenv("I_SHARE_CLIENT_ID") + defaultIShareIdpID = os.Getenv("I_SHARE_IDP_ID") } func main() { @@ -72,7 +75,7 @@ func main() { router.Run(fmt.Sprintf("0.0.0.0:%v", serverPort)) log.Infof("Started router at %v", serverPort) } else { - token, _ := generateToken() + token, _ := generateToken(defaultIShareClientID, defaultIShareIdpID) log.Infof("Token: %s", token) } @@ -83,7 +86,18 @@ type Token struct { } func token(c *gin.Context) { - token, err := generateToken() + clientId := c.Query("clientId") + idpId := c.Query("idpId") + + if clientId == "" { + clientId = defaultIShareClientID + } + if idpId == "" { + idpId = defaultIShareIdpID + } + log.Infof("Creating token for %s - %s", clientId, idpId) + + token, err := generateToken(clientId, idpId) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) @@ -92,7 +106,7 @@ func token(c *gin.Context) { c.AbortWithStatusJSON(http.StatusOK, Token{token}) } -func generateToken() (token string, err error) { +func generateToken(clientId string, idpId string) (token string, err error) { randomUuid, err := uuid.NewRandom() if err != nil { @@ -104,14 +118,21 @@ func generateToken() (token string, err error) { now := time.Now().Unix() jwtToken := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ "jti": randomUuid.String(), - "iss": iShareClientID, - "sub": iShareClientID, - "aud": iShareIdpID, + "iss": clientId, + "sub": clientId, + "aud": idpId, "iat": now, "exp": now + 30, }) - key, err := getSigningKey(keyPath) + credential := credentials[clientId] + + if credential == (Credential{}) { + log.Errorf("No credentials for %s exist.", clientId) + return token, errors.New("no_such_credentials") + } + + key, err := getSigningKey(credential.Key) if err != nil { log.Warn("Was not able to read the signing key.") return @@ -121,7 +142,7 @@ func generateToken() (token string, err error) { return } - cert, err := getEncodedCertificate(certificatePath) + cert, err := getEncodedCertificate(credential.Certificate) if err != nil { log.Warn("Was not able to read the certificate.") return