├── .chglog ├── CHANGELOG.tpl.md └── config.yml ├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .golangci.yaml ├── .license-ranger.json ├── CHANGELOG.md ├── Caddyfile.dev ├── Caddyfile.example ├── Dockerfile ├── Dockerfile.build ├── Dockerfile.release ├── Jenkinsfile ├── LICENSE.txt ├── Makefile ├── NOTICE.txt ├── README.md ├── bootstrap ├── bootstrap.go ├── cookie.go ├── dummy.go ├── guest.go ├── kc.go ├── ldap.go ├── managers.go └── utils.go ├── claims.go ├── cmd ├── konnectd │ ├── env.go │ ├── healthcheck.go │ ├── jwk.go │ ├── log.go │ ├── main.go │ ├── serve.go │ └── utils.go ├── root.go └── version.go ├── config └── config.go ├── context.go ├── doc.go ├── docs └── konnect-identifier-api-v1.yaml ├── encryption ├── encryption.go ├── encryption_test.go └── generate.go ├── examples └── coookieserver.py ├── go.mod ├── go.sum ├── identifier-registration.yaml.in ├── identifier ├── .env ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── Makefile ├── README.md ├── README.react.md ├── api.go ├── backends │ ├── backend.go │ ├── kc.go │ ├── kc │ │ └── consts.go │ ├── ldap.go │ └── ldap │ │ └── const.go ├── claims.go ├── config.go ├── cookie.go ├── flows.go ├── handlers.go ├── i18n │ ├── Makefile │ ├── de.po │ ├── es.po │ ├── fr.po │ ├── hi.po │ ├── hr.po │ ├── hu.po │ ├── is.po │ ├── it.po │ ├── ja.po │ ├── konnect-identifier.pot │ ├── nb.po │ ├── nl.po │ ├── pl.po │ ├── pt_PT.po │ ├── ru.po │ └── sl.po ├── identifier.go ├── meta │ ├── meta.go │ └── scopes │ │ ├── definition.go │ │ └── scopes.go ├── models.go ├── modes.go ├── oauth2.go ├── package.json ├── public │ ├── index.html │ └── static │ │ └── favicon.ico ├── saml2.go ├── src │ ├── Main.js │ ├── Main.test.js │ ├── Makefile │ ├── Routes.js │ ├── actions │ │ ├── common.js │ │ ├── login.js │ │ ├── types.js │ │ └── utils.js │ ├── app.css │ ├── app.js │ ├── components │ │ ├── ClientDisplayName.js │ │ ├── Loading.js │ │ ├── PrivateRoute.js │ │ ├── RedirectWithQuery.js │ │ ├── ResponsiveDialog.js │ │ ├── ResponsiveScreen.js │ │ └── ScopesList.js │ ├── containers │ │ ├── Goodbye │ │ │ ├── Goodbyescreen.js │ │ │ └── index.js │ │ ├── Login │ │ │ ├── Chooseaccount.js │ │ │ ├── Consent.js │ │ │ ├── Login.js │ │ │ ├── Loginscreen.js │ │ │ └── index.js │ │ └── Welcome │ │ │ ├── Welcomescreen.js │ │ │ └── index.js │ ├── errors │ │ └── index.js │ ├── fancy-background.css │ ├── images │ │ ├── .gitignore │ │ ├── Makefile │ │ ├── kopano-augmenting-teamwork-bg-7680.jpg │ │ ├── kopano-augmenting-teamwork-overlay.svg │ │ ├── kopano-konnect-icon.svg │ │ ├── kopano-logo.svg │ │ ├── loginscreen-bg-thumb.svg.in │ │ └── loginscreen-bg.css.in │ ├── index.js │ ├── locales │ │ └── index.js │ ├── models │ │ └── hello.js │ ├── reducers │ │ ├── common.js │ │ ├── index.js │ │ └── login.js │ ├── store.js │ ├── utils.js │ └── version.js ├── trampolin.go ├── user.go ├── utils.go └── yarn.lock ├── identity ├── auth.go ├── authorities │ ├── authorities.go │ ├── models.go │ ├── oidc.go │ ├── registry.go │ ├── saml2.go │ └── samlext │ │ ├── dsig.go │ │ ├── idplogoutrequest.go │ │ ├── idplogoutresponse.go │ │ ├── logoutrequest.go │ │ └── logoutresponse.go ├── authrecord.go ├── clients │ ├── claims.go │ ├── clients.go │ ├── models.go │ ├── registry.go │ └── registry_test.go ├── config.go ├── context.go ├── errors.go ├── manager.go ├── managers │ ├── cookie.go │ ├── dummy.go │ ├── encryption.go │ ├── guest.go │ ├── identifier.go │ └── utils.go ├── user.go └── utils.go ├── managers └── managers.go ├── oidc ├── claims.go ├── code │ ├── manager.go │ └── managers │ │ └── memorymap.go ├── errors.go ├── oidc.go ├── payload │ ├── authentication.go │ ├── claims.go │ ├── endsession.go │ ├── registration.go │ ├── request.go │ ├── schema.go │ ├── session.go │ ├── token.go │ ├── userinfo.go │ └── utils.go └── provider │ ├── config.go │ ├── cookie.go │ ├── handlers.go │ ├── handlers_test.go │ ├── html.go │ ├── identity.go │ ├── provider.go │ ├── provider_test.go │ ├── session.go │ ├── signing.go │ ├── state.go │ ├── subject.go │ ├── tokens.go │ └── utils.go ├── payloads.go ├── provider.go ├── scopes.go ├── scopes.yaml.in ├── scripts ├── README.md ├── build-konnectd-aci.sh ├── docker-entrypoint.sh ├── go-license-ranger.py ├── healthcheck.sh ├── js-license-ranger.js ├── konnectd.cfg ├── kopano-konnectd.binscript └── kopano-konnectd.service ├── server ├── config.go ├── handlers.go ├── handlers_test.go ├── server.go └── server_test.go ├── signing ├── jwk.go └── jwt.go ├── utils ├── errorpage.go ├── errors.go ├── http.go ├── json.go ├── origin.go ├── redirect.go ├── schema.go ├── trusted.go └── utils.go └── version └── version.go /.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | {{ if .Versions -}} 4 | ## Unreleased 5 | {{ if .Unreleased.CommitGroups -}} 6 | {{ range .Unreleased.CommitGroups -}} 7 | ### {{ .Title }} 8 | {{ range .Commits -}} 9 | {{ if not (hasSuffix (lower .Subject) " changelog") -}} 10 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 11 | {{ end -}} 12 | {{ end }} 13 | {{ end -}} 14 | {{ else }} 15 | {{ range .Unreleased.Commits -}} 16 | {{ if not (hasSuffix (lower .Subject) " changelog") -}} 17 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 18 | {{ end -}} 19 | {{ end }} 20 | {{ end -}} 21 | {{ end -}} 22 | 23 | {{ range .Versions }} 24 | ## {{ if .Tag.Previous }}{{ .Tag.Name }}{{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }}) 25 | {{ if .CommitGroups -}} 26 | {{ range .CommitGroups -}} 27 | ### {{ .Title }} 28 | {{ range .Commits -}} 29 | {{ if not (hasSuffix (lower .Subject) " changelog") -}} 30 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 31 | {{ end -}} 32 | {{ end }} 33 | {{ end -}} 34 | {{ else }} 35 | {{ range .Commits -}} 36 | {{ if not (hasSuffix (lower .Subject) " changelog") -}} 37 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 38 | {{ end -}} 39 | {{ end }} 40 | {{ end -}} 41 | 42 | {{- if .NoteGroups -}} 43 | {{ range .NoteGroups -}} 44 | ### {{ .Title }} 45 | {{ range .Notes }} 46 | {{ .Body }} 47 | {{ end }} 48 | {{ end -}} 49 | {{ end -}} 50 | {{ end -}} 51 | -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: bitbucket 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://stash.kopano.io/projects/KC/repos/konnect/ 6 | options: 7 | commits: 8 | # filters: 9 | # Type: 10 | # - feat 11 | # - fix 12 | # - perf 13 | # - refactor 14 | commit_groups: 15 | # title_maps: 16 | # feat: Features 17 | # fix: Bug Fixes 18 | # perf: Performance Improvements 19 | # refactor: Code Refactoring 20 | header: 21 | pattern: "^(.*)$" 22 | pattern_maps: 23 | - Subject 24 | notes: 25 | keywords: 26 | - BREAKING CHANGE 27 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ** 2 | !bin/konnectd 3 | !scripts/*.sh 4 | !identifier/build 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | indent_style = tab 10 | indent_size = 4 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | ; Python: PEP8 defines 4 spaces for indentation 15 | [*.py] 16 | indent_style = space 17 | indent_size = 4 18 | 19 | ; YAML format, 2 spaces 20 | [*.{yaml,yml,yaml.in}] 21 | indent_style = space 22 | indent_size = 2 23 | 24 | ; HTML, CSS and JavaScript, 4 spaces 25 | [*.{html,css,js}] 26 | charset = utf-8 27 | indent_size = 2 28 | indent_style = space 29 | charset = utf-8 30 | end_of_line = lf 31 | insert_final_newline = true 32 | trim_trailing_whitespace = true 33 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "extends": "eslint:recommended", 11 | "env": { 12 | "browser": true, 13 | "es6": true 14 | }, 15 | "rules": { 16 | "max-len": [ 17 | "warn", 18 | 120 19 | ], 20 | "indent": [ 21 | "error", 22 | 2, 23 | { 24 | "SwitchCase": 1 25 | } 26 | ], 27 | "camelcase": [ 28 | "error", 29 | { 30 | "properties": "always" 31 | } 32 | ], 33 | "key-spacing": [ 34 | "error", 35 | { 36 | "beforeColon": false, 37 | "afterColon": true 38 | } 39 | ], 40 | "comma-dangle": [ 41 | "error", 42 | "never" 43 | ], 44 | "dot-notation": [ 45 | "error" 46 | ], 47 | "semi": [ 48 | "error", 49 | "always" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /bin 3 | /.gopath 4 | /.vscode 5 | /cmd/konnectd/debug 6 | /test/tests.* 7 | /test/coverage.* 8 | /golint.txt 9 | /govet.txt 10 | /dist 11 | /examples 12 | /identifier/node_modules 13 | /Caddyfile 14 | /3rdparty-LICENSES.md 15 | /identifier-registration.yaml 16 | /scopes.yaml 17 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | modules-download-mode: vendor 3 | 4 | linters-settings: 5 | govet: 6 | check-shadowing: true 7 | settings: 8 | printf: 9 | funcs: 10 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 11 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 12 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 13 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 14 | golint: 15 | min-confidence: 0 16 | gocyclo: 17 | min-complexity: 10 18 | maligned: 19 | suggest-new: true 20 | dupl: 21 | threshold: 100 22 | goconst: 23 | min-len: 2 24 | min-occurrences: 2 25 | misspell: 26 | locale: US 27 | lll: 28 | line-length: 200 29 | goimports: 30 | local-prefixes: stash.kopano.io/kc/kapi 31 | gocritic: 32 | enabled-tags: 33 | - performance 34 | - style 35 | - experimental 36 | 37 | linters: 38 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 39 | disable-all: true 40 | enable: 41 | - bodyclose 42 | - deadcode 43 | - dupl 44 | - errcheck 45 | - funlen 46 | - gochecknoinits 47 | - goconst 48 | - gocritic 49 | - gocyclo 50 | - gofmt 51 | - goimports 52 | - golint 53 | - gosec 54 | - gosimple 55 | - govet 56 | - ineffassign 57 | - interfacer 58 | - lll 59 | - misspell 60 | - nakedret 61 | - scopelint 62 | - staticcheck 63 | - structcheck 64 | - stylecheck 65 | - typecheck 66 | - unconvert 67 | - unparam 68 | - unused 69 | - varcheck 70 | 71 | # don't enable: 72 | # - depguard - until https://github.com/OpenPeeDeeP/depguard/issues/7 gets fixed 73 | # - maligned,prealloc 74 | # - gochecknoglobals 75 | -------------------------------------------------------------------------------- /.license-ranger.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "mod-vendor", 3 | "header": "# Kopano Konnect 3rd party licenses\n\nCopyright 2019 Kopano and its licensors. See LICENSE.txt for license information. This document contains a list of open source components used in this project.\n\n## Konnect konnectd\n" 4 | } 5 | -------------------------------------------------------------------------------- /Caddyfile.dev: -------------------------------------------------------------------------------- 1 | # Example Caddyfile to use with https://caddyserver.com 2 | # 3 | # This assumes Konnect is running with identifier on 127.0.0.1:8777. In addition 4 | # for development, the identifier is used directly from webpack-dev-server 5 | # running on 127.0.0.1:3001. Additional examples are included for third party 6 | # login provides which use cookie passthrough backend. 7 | 8 | 0.0.0.0:8443 { 9 | errors stderr 10 | log stdout 11 | 12 | tls self_signed 13 | 14 | # konnect oidc 15 | proxy /.well-known/openid-configuration 127.0.0.1:8777 16 | proxy /konnect/v1/jwks.json 127.0.0.1:8777 17 | proxy /konnect/v1/token 127.0.0.1:8777 18 | proxy /konnect/v1/userinfo 127.0.0.1:8777 19 | proxy /konnect/v1/static 127.0.0.1:8777 20 | proxy /konnect/v1/session 127.0.0.1:8777 21 | proxy /konnect/v1/register 127.0.0.1:8777 22 | 23 | # konnect identifier development via webpack-dev-server 24 | proxy /signin/v1/ 127.0.0.1:3001 { 25 | header_downstream Cache-Control "no-cache, max-age=0, public" 26 | header_downstream Referrer-Policy origin 27 | header_downstream Content-Security-Policy "object-src 'none'; script-src 'self'; base-uri 'none'; frame-ancestors 'none';" 28 | } 29 | proxy /sockjs-node 127.0.0.1:3001 { 30 | websocket 31 | } 32 | proxy /static 127.0.0.1:3001 33 | proxy /signin/v1/identifier/_/ 127.0.0.1:8777 { 34 | transparent 35 | } 36 | 37 | # konnect identifier login area 38 | proxy /signin/ 127.0.0.1:8777 { 39 | transparent 40 | } 41 | 42 | # third party login area provider example 43 | # proxy /provider/simple 127.0.0.1:8999 44 | 45 | # konnect authorize endpoint below third party login area provider 46 | #proxy /provider/simple/konnect/v1/authorize 127.0.0.1:8777 { 47 | # without /provider/simple 48 | # header_upstream X-Forwarded-Prefix /provider/simple 49 | #} 50 | 51 | # konnect cookieserver, start with python3 ./examples/cookieserver.py 8088 52 | #proxy /cookieserver/simple-userinfo 127.0.0.1:8088 53 | } 54 | -------------------------------------------------------------------------------- /Caddyfile.example: -------------------------------------------------------------------------------- 1 | # Example Caddyfile to use with https://caddyserver.com 2 | # 3 | # This assumes Konnect is running with identifier on 127.0.0.1:8777. 4 | 5 | 0.0.0.0:8443 { 6 | errors stderr 7 | log stdout 8 | 9 | tls self_signed 10 | 11 | # konnect oidc 12 | proxy /.well-known/openid-configuration 127.0.0.1:8777 13 | proxy /konnect/v1/jwks.json 127.0.0.1:8777 14 | proxy /konnect/v1/token 127.0.0.1:8777 15 | proxy /konnect/v1/userinfo 127.0.0.1:8777 16 | proxy /konnect/v1/static 127.0.0.1:8777 17 | proxy /konnect/v1/session 127.0.0.1:8777 18 | proxy /konnect/v1/register 127.0.0.1:8777 19 | 20 | # konnect identifier login area 21 | proxy /signin/ 127.0.0.1:8777 { 22 | transparent 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Kopano and its licensors 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License, version 3 or 6 | # later, as published by the Free Software Foundation. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | # 16 | 17 | FROM golang:1.14.10-buster 18 | 19 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 20 | 21 | ARG GOLANGCI_LINT_TAG=v1.23.8 22 | RUN curl -sfL \ 23 | https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \ 24 | sh -s -- -b /usr/local/bin ${GOLANGCI_LINT_TAG} 25 | 26 | RUN GOBIN=/usr/local/bin go get -v \ 27 | github.com/tebeka/go2xunit \ 28 | github.com/axw/gocov/... \ 29 | github.com/AlekSi/gocov-xml \ 30 | github.com/wadey/gocovmerge \ 31 | && go clean -cache && rm -rf /root/go 32 | 33 | ENV DEBIAN_FRONTEND noninteractive 34 | 35 | RUN apt-get update \ 36 | && apt-get install -y --no-install-recommends \ 37 | build-essential \ 38 | gettext-base \ 39 | imagemagick \ 40 | python-scour \ 41 | nodejs \ 42 | && apt-get clean \ 43 | && rm -rf /var/lib/apt/lists/* 44 | 45 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 46 | RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list 47 | RUN apt-get update \ 48 | && apt-get install -y --no-install-recommends \ 49 | yarn \ 50 | && apt-get clean \ 51 | && rm -rf /var/lib/apt/lists/* 52 | 53 | WORKDIR /build 54 | 55 | ENV GOCACHE=/tmp/go-build 56 | ENV GOPATH="" 57 | ENV HOME=/tmp 58 | 59 | CMD ["make", "DATE=reproducible"] 60 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | pipeline { 4 | agent { 5 | dockerfile { 6 | filename 'Dockerfile.build' 7 | } 8 | } 9 | stages { 10 | stage('Bootstrap') { 11 | steps { 12 | echo 'Bootstrapping..' 13 | sh 'go version' 14 | sh 'yarn -v' 15 | sh 'node -v' 16 | } 17 | } 18 | stage('Lint') { 19 | steps { 20 | echo 'Linting..' 21 | sh 'make lint-checkstyle GOLINT_ARGS="--new-from-rev=HEAD~ --verbose"' 22 | } 23 | } 24 | stage('Test') { 25 | steps { 26 | echo 'Testing..' 27 | sh 'make test-xml-short' 28 | } 29 | } 30 | stage('Vendor') { 31 | steps { 32 | echo 'Fetching vendor dependencies..' 33 | sh 'make vendor' 34 | } 35 | } 36 | stage('Build') { 37 | steps { 38 | echo 'Building..' 39 | sh 'make DATE=reproducible' 40 | sh './bin/konnectd version && sha256sum ./bin/konnectd' 41 | } 42 | } 43 | stage('Test with coverage') { 44 | steps { 45 | echo 'Testing with coverage..' 46 | sh 'make test-coverage COVERAGE_DIR=test/coverage.jenkins || true' 47 | publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: 'test/coverage.jenkins', reportFiles: 'coverage.html', reportName: 'Go Coverage Report HTML', reportTitles: '']) 48 | step([$class: 'CoberturaPublisher', autoUpdateHealth: false, autoUpdateStability: false, coberturaReportFile: 'test/coverage.jenkins/coverage.xml', failUnhealthy: false, failUnstable: false, maxNumberOfBuilds: 0, onlyStable: false, sourceEncoding: 'ASCII', zoomCoverageChart: false]) 49 | } 50 | } 51 | stage('Dist') { 52 | steps { 53 | echo 'Dist..' 54 | sh '$(git diff --stat)' 55 | sh 'test -z "$(git diff --shortstat 2>/dev/null |tail -n1)" && echo "Clean check passed."' 56 | sh 'make check' 57 | sh 'make dist' 58 | } 59 | } 60 | } 61 | post { 62 | always { 63 | junit allowEmptyResults: false, testResults: 'test/tests.xml' 64 | 65 | recordIssues enabledForFailure: true, qualityGates: [[threshold: 100, type: 'TOTAL', unstable: true]], tools: [checkStyle(id: 'golint', name: 'Golint', pattern: 'test/tests.lint.xml'), checkStyle(id: 'eslint', name: 'ESLint', pattern: 'test/tests.eslint.xml')] 66 | 67 | archiveArtifacts 'dist/*.tar.gz' 68 | cleanWs() 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Kopano Konnect 2 | Copyright 2017-2019 Kopano and its licensors 3 | 4 | This product includes software developed at 5 | Kopano (https://kopano.com/). 6 | -------------------------------------------------------------------------------- /bootstrap/cookie.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package bootstrap 19 | 20 | import ( 21 | "fmt" 22 | "net/url" 23 | "strings" 24 | "time" 25 | 26 | "github.com/sirupsen/logrus" 27 | 28 | "stash.kopano.io/kc/konnect/identity" 29 | identityManagers "stash.kopano.io/kc/konnect/identity/managers" 30 | ) 31 | 32 | func newCookieIdentityManager(bs *bootstrap, cfg *Config) (identity.Manager, error) { 33 | logger := bs.cfg.Logger 34 | 35 | if bs.authorizationEndpointURI.EscapedPath() == "" { 36 | bs.authorizationEndpointURI.Path = bs.makeURIPath(apiTypeKonnect, "/authorize") 37 | } 38 | 39 | if !strings.HasPrefix(bs.signInFormURI.EscapedPath(), "/") { 40 | return nil, fmt.Errorf("URI path must be absolute") 41 | } 42 | 43 | if cfg.CookieBackendURI == "" { 44 | return nil, fmt.Errorf("cookie backend requires the backend URI as argument") 45 | } 46 | backendURI, backendURIErr := url.Parse(cfg.CookieBackendURI) 47 | if backendURIErr != nil || !backendURI.IsAbs() { 48 | if backendURIErr == nil { 49 | backendURIErr = fmt.Errorf("URI must have a scheme") 50 | } 51 | return nil, fmt.Errorf("invalid backend URI, %v", backendURIErr) 52 | } 53 | 54 | var cookieNames []string 55 | if len(cfg.CookieNames) > 0 { 56 | // TODO(longsleep): Add proper usage help. 57 | cookieNames = cfg.CookieNames 58 | } 59 | 60 | identityManagerConfig := &identity.Config{ 61 | SignInFormURI: bs.signInFormURI, 62 | 63 | Logger: logger, 64 | 65 | ScopesSupported: bs.cfg.AllowedScopes, 66 | } 67 | 68 | cookieIdentityManager := identityManagers.NewCookieIdentityManager(identityManagerConfig, backendURI, cookieNames, 30*time.Second, bs.cfg.HTTPTransport) 69 | logger.WithFields(logrus.Fields{ 70 | "backend": backendURI, 71 | "signIn": bs.signInFormURI, 72 | "cookies": cookieNames, 73 | }).Infoln("using cookie backed identity manager") 74 | 75 | return cookieIdentityManager, nil 76 | } 77 | -------------------------------------------------------------------------------- /bootstrap/dummy.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package bootstrap 19 | 20 | import ( 21 | "stash.kopano.io/kc/konnect/identity" 22 | identityManagers "stash.kopano.io/kc/konnect/identity/managers" 23 | ) 24 | 25 | func newDummyIdentityManager(bs *bootstrap) (identity.Manager, error) { 26 | logger := bs.cfg.Logger 27 | 28 | identityManagerConfig := &identity.Config{ 29 | Logger: logger, 30 | 31 | ScopesSupported: bs.cfg.AllowedScopes, 32 | } 33 | 34 | sub := "dummy" 35 | dummyIdentityManager := identityManagers.NewDummyIdentityManager(identityManagerConfig, sub) 36 | logger.WithField("sub", sub).Warnln("using dummy identity manager") 37 | 38 | return dummyIdentityManager, nil 39 | } 40 | -------------------------------------------------------------------------------- /bootstrap/guest.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package bootstrap 19 | 20 | import ( 21 | "stash.kopano.io/kc/konnect/identity" 22 | identityManagers "stash.kopano.io/kc/konnect/identity/managers" 23 | ) 24 | 25 | func newGuestIdentityManager(bs *bootstrap) (identity.Manager, error) { 26 | logger := bs.cfg.Logger 27 | 28 | identityManagerConfig := &identity.Config{ 29 | Logger: logger, 30 | } 31 | 32 | guestIdentityManager := identityManagers.NewGuestIdentityManager(identityManagerConfig) 33 | 34 | return guestIdentityManager, nil 35 | } 36 | -------------------------------------------------------------------------------- /bootstrap/managers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package bootstrap 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | "time" 24 | 25 | identityAuthorities "stash.kopano.io/kc/konnect/identity/authorities" 26 | identityClients "stash.kopano.io/kc/konnect/identity/clients" 27 | identityManagers "stash.kopano.io/kc/konnect/identity/managers" 28 | "stash.kopano.io/kc/konnect/managers" 29 | codeManagers "stash.kopano.io/kc/konnect/oidc/code/managers" 30 | ) 31 | 32 | func newManagers(ctx context.Context, bs *bootstrap) (*managers.Managers, error) { 33 | logger := bs.cfg.Logger 34 | 35 | var err error 36 | mgrs := managers.New() 37 | 38 | // Encryption manager. 39 | encryption, err := identityManagers.NewEncryptionManager(nil) 40 | if err != nil { 41 | return nil, fmt.Errorf("failed to create encryption manager: %v", err) 42 | } 43 | 44 | err = encryption.SetKey(bs.encryptionSecret) 45 | if err != nil { 46 | return nil, fmt.Errorf("invalid --encryption-secret parameter value for encryption: %v", err) 47 | } 48 | mgrs.Set("encryption", encryption) 49 | logger.Infof("encryption set up with %d key size", encryption.GetKeySize()) 50 | 51 | // OIDC code manage. 52 | code := codeManagers.NewMemoryMapManager(ctx) 53 | mgrs.Set("code", code) 54 | 55 | // Identifier client registry manager. 56 | clients, err := identityClients.NewRegistry(ctx, bs.issuerIdentifierURI, bs.identifierRegistrationConf, bs.cfg.AllowDynamicClientRegistration, time.Duration(bs.dyamicClientSecretDurationSeconds)*time.Second, logger) 57 | if err != nil { 58 | return nil, fmt.Errorf("failed to create client registry: %v", err) 59 | } 60 | mgrs.Set("clients", clients) 61 | 62 | // Identifier authorities registry manager. 63 | authorities, err := identityAuthorities.NewRegistry(ctx, bs.makeURI(apiTypeSignin, ""), bs.identifierAuthoritiesConf, logger) 64 | if err != nil { 65 | return nil, fmt.Errorf("failed to create authorities registry: %v", err) 66 | } 67 | mgrs.Set("authorities", authorities) 68 | 69 | return mgrs, nil 70 | } 71 | -------------------------------------------------------------------------------- /cmd/konnectd/env.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package main 19 | 20 | import ( 21 | "os" 22 | "strings" 23 | ) 24 | 25 | // envOrDefault returns the value of an env-variable or the default if the env-var is not set 26 | func envOrDefault(name string, def string) string { 27 | v := os.Getenv(name) 28 | if v == "" { 29 | return def 30 | } 31 | 32 | return v 33 | } 34 | 35 | // listEnvArg parses an env-arg which has a space separated list as value 36 | func listEnvArg(name string) []string { 37 | list := make([]string, 0) 38 | for _, keyFn := range strings.Split(os.Getenv(name), " ") { 39 | keyFn = strings.TrimSpace(keyFn) 40 | if keyFn != "" { 41 | list = append(list, keyFn) 42 | } 43 | } 44 | 45 | return list 46 | } 47 | -------------------------------------------------------------------------------- /cmd/konnectd/healthcheck.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package main 19 | 20 | import ( 21 | "context" 22 | "crypto/tls" 23 | "fmt" 24 | "io/ioutil" 25 | "net/http" 26 | "net/url" 27 | "os" 28 | "time" 29 | 30 | "github.com/spf13/cobra" 31 | 32 | "stash.kopano.io/kc/konnect/utils" 33 | ) 34 | 35 | func commandHealthcheck() *cobra.Command { 36 | healthcheckCmd := &cobra.Command{ 37 | Use: "healthcheck", 38 | Short: "Konnect server health check", 39 | Run: func(cmd *cobra.Command, args []string) { 40 | if err := healthcheck(cmd, args); err != nil { 41 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 42 | os.Exit(1) 43 | } 44 | }, 45 | } 46 | 47 | healthcheckCmd.Flags().String("hostname", defaultListenAddr, "Host and port where konnectd is listening") 48 | healthcheckCmd.Flags().String("path", "/health-check", "URL path and optional parameters to health-check endpoint") 49 | healthcheckCmd.Flags().String("scheme", "http", "URL scheme") 50 | healthcheckCmd.Flags().Bool("insecure", false, "Disable TLS certificate and hostname validation") 51 | 52 | return healthcheckCmd 53 | } 54 | 55 | func healthcheck(cmd *cobra.Command, args []string) error { 56 | ctx := context.Background() 57 | 58 | uri := url.URL{} 59 | uri.Scheme, _ = cmd.Flags().GetString("scheme") 60 | uri.Host, _ = cmd.Flags().GetString("hostname") 61 | uri.Path, _ = cmd.Flags().GetString("path") 62 | 63 | var tlsClientConfig *tls.Config 64 | if insecure, _ := cmd.Flags().GetBool("insecure"); insecure { 65 | tlsClientConfig = utils.InsecureSkipVerifyTLSConfig() 66 | } 67 | client := http.Client{ 68 | Timeout: time.Second * 60, 69 | Transport: utils.HTTPTransportWithTLSClientConfig(tlsClientConfig), 70 | } 71 | 72 | request, err := http.NewRequest(http.MethodPost, uri.String(), nil) 73 | if err != nil { 74 | return fmt.Errorf("failed to create healthcheck request: %v", err) 75 | } 76 | 77 | request.Header.Set("Connection", "close") 78 | request.Header.Set("User-Agent", utils.DefaultHTTPUserAgent) 79 | request = request.WithContext(ctx) 80 | 81 | response, err := client.Do(request) 82 | if err != nil { 83 | return fmt.Errorf("healthcheck request failed: %v", err) 84 | } 85 | defer response.Body.Close() 86 | 87 | if response.StatusCode != http.StatusOK { 88 | bodyBytes, _ := ioutil.ReadAll(response.Body) 89 | fmt.Fprintf(os.Stderr, string(bodyBytes)) 90 | 91 | return fmt.Errorf("healthcheck failed with status: %v", response.StatusCode) 92 | } else { 93 | fmt.Fprintf(os.Stdout, "healthcheck successful\n") 94 | } 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /cmd/konnectd/jwk.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package main 19 | 20 | import ( 21 | "bytes" 22 | "encoding/json" 23 | "fmt" 24 | "os" 25 | "path/filepath" 26 | "strings" 27 | 28 | "github.com/ghodss/yaml" 29 | "github.com/spf13/cobra" 30 | "gopkg.in/square/go-jose.v2" 31 | 32 | "stash.kopano.io/kc/konnect/bootstrap" 33 | ) 34 | 35 | func commandJwkFromPem() *cobra.Command { 36 | cmd := &cobra.Command{ 37 | Use: "jwk-from-pem [key.pem]", 38 | Short: "Create JSON Web Key from PEM key file", 39 | Run: func(cmd *cobra.Command, args []string) { 40 | if err := jwkFromPem(cmd, args); err != nil { 41 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 42 | os.Exit(1) 43 | } 44 | }, 45 | } 46 | 47 | cmd.Flags().String("kid", "", "Key ID kid") 48 | cmd.Flags().String("use", "sig", "Key usage use (required)") 49 | cmd.Flags().Bool("yaml", false, "Output JWK as YAML") 50 | 51 | return cmd 52 | } 53 | 54 | func jwkFromPem(cmd *cobra.Command, args []string) error { 55 | if len(args) != 1 { 56 | cmd.Help() 57 | os.Exit(2) 58 | } 59 | 60 | kid, _ := cmd.Flags().GetString("kid") 61 | use, _ := cmd.Flags().GetString("use") 62 | asYaml, _ := cmd.Flags().GetBool("yaml") 63 | fn := args[0] 64 | 65 | key, err := func() (interface{}, error) { 66 | signerKid, signer, err := bootstrap.LoadSignerFromFile(fn) 67 | if err == nil { 68 | if kid == "" { 69 | kid = signerKid 70 | } 71 | return signer, nil 72 | } 73 | validatorKid, validator, err := bootstrap.LoadValidatorFromFile(fn) 74 | if err == nil { 75 | if kid == "" { 76 | kid = validatorKid 77 | } 78 | return validator, nil 79 | } 80 | return nil, err 81 | }() 82 | if err != nil { 83 | return fmt.Errorf("failed to load pem file: %v", err) 84 | } 85 | 86 | if kid == "" { 87 | // Use file name as kid if no kid was given. 88 | _, fn := filepath.Split(fn) 89 | kid = strings.TrimSuffix(fn, filepath.Ext(fn)) 90 | } 91 | 92 | priv := jose.JSONWebKey{Key: key, KeyID: kid, Use: use} 93 | if !priv.Valid() { 94 | return fmt.Errorf("parsed key is not valid") 95 | } 96 | 97 | privJSON, err := priv.MarshalJSON() 98 | if err != nil { 99 | return fmt.Errorf("error marshaling key as JSON: %v", err) 100 | } 101 | 102 | if asYaml { 103 | privYAML, err := yaml.JSONToYAML(privJSON) 104 | if err != nil { 105 | return fmt.Errorf("error marshalling key as YAML: %v", err) 106 | } 107 | fmt.Println(string(privYAML)) 108 | } else { 109 | var prettyPrivJSON bytes.Buffer 110 | err = json.Indent(&prettyPrivJSON, privJSON, "", "\t") 111 | if err != nil { 112 | return fmt.Errorf("error marshalling key as pretty JSON: %v", err) 113 | } 114 | fmt.Println(prettyPrivJSON.String()) 115 | } 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /cmd/konnectd/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package main 19 | 20 | import ( 21 | "os" 22 | 23 | "github.com/sirupsen/logrus" 24 | ) 25 | 26 | func newLogger(disableTimestamp bool, logLevelString string) (logrus.FieldLogger, error) { 27 | logLevel, err := logrus.ParseLevel(logLevelString) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return &logrus.Logger{ 33 | Out: os.Stderr, 34 | Formatter: &logrus.TextFormatter{ 35 | DisableTimestamp: disableTimestamp, 36 | }, 37 | Level: logLevel, 38 | }, nil 39 | } 40 | -------------------------------------------------------------------------------- /cmd/konnectd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "os" 23 | 24 | "stash.kopano.io/kc/konnect/cmd" 25 | ) 26 | 27 | // Defaults. 28 | const ( 29 | defaultListenAddr = "127.0.0.1:8777" 30 | defaultIdentifierClientPath = "./identifier-webapp" 31 | ) 32 | 33 | func main() { 34 | cmd.RootCmd.AddCommand(commandServe()) 35 | cmd.RootCmd.AddCommand(commandUtils()) 36 | cmd.RootCmd.AddCommand(commandHealthcheck()) 37 | 38 | if err := cmd.RootCmd.Execute(); err != nil { 39 | fmt.Fprintln(os.Stderr, err.Error()) 40 | os.Exit(1) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cmd/konnectd/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package main 19 | 20 | import ( 21 | "os" 22 | 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | func commandUtils() *cobra.Command { 27 | jwkCmd := &cobra.Command{ 28 | Use: "utils", 29 | Short: "Konnect related utilities", 30 | Run: func(cmd *cobra.Command, args []string) { 31 | cmd.Help() 32 | os.Exit(2) 33 | }, 34 | } 35 | 36 | jwkCmd.AddCommand(commandJwkFromPem()) 37 | 38 | return jwkCmd 39 | } 40 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package cmd 19 | 20 | import ( 21 | "os" 22 | 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | // RootCmd provides the commandline parser root. 27 | var RootCmd = &cobra.Command{ 28 | Use: "konnectd", 29 | Run: func(cmd *cobra.Command, args []string) { 30 | cmd.Help() 31 | os.Exit(2) 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package cmd 19 | 20 | import ( 21 | "fmt" 22 | "runtime" 23 | 24 | "github.com/spf13/cobra" 25 | 26 | "stash.kopano.io/kc/konnect/version" 27 | ) 28 | 29 | // CommandVersion provides the commandline implementation for version. 30 | func CommandVersion() *cobra.Command { 31 | versionCmd := &cobra.Command{ 32 | Use: "version", 33 | Short: "Print the version and exit", 34 | Run: func(cmd *cobra.Command, args []string) { 35 | fmt.Printf(`Version : %s 36 | Build date : %s 37 | Built with : %s %s/%s 38 | `, 39 | version.Version, version.BuildDate, runtime.Version(), runtime.GOOS, runtime.GOARCH) 40 | }, 41 | } 42 | 43 | return versionCmd 44 | } 45 | 46 | func init() { 47 | RootCmd.AddCommand(CommandVersion()) 48 | } 49 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package config 19 | 20 | import ( 21 | "net" 22 | "net/http" 23 | 24 | "github.com/sirupsen/logrus" 25 | ) 26 | 27 | // Config defines a Server's configuration settings. 28 | type Config struct { 29 | ListenAddr string 30 | 31 | WithMetrics bool 32 | 33 | Logger logrus.FieldLogger 34 | HTTPTransport http.RoundTripper 35 | 36 | TrustedProxyIPs []*net.IP 37 | TrustedProxyNets []*net.IPNet 38 | 39 | AllowedScopes []string 40 | AllowClientGuests bool 41 | AllowDynamicClientRegistration bool 42 | } 43 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package konnect 19 | 20 | import ( 21 | "context" 22 | 23 | "github.com/dgrijalva/jwt-go" 24 | ) 25 | 26 | // key is an unexported type for keys defined in this package. 27 | // This prevents collisions with keys defined in other packages. 28 | type key int 29 | 30 | // claimsKey is the key for claims in contexts. It is 31 | // unexported; clients use konnect.NewClaimsContext and 32 | // connect.FromClaimsContext instead of using this key directly. 33 | var claimsKey key 34 | 35 | // NewClaimsContext returns a new Context that carries value auth. 36 | func NewClaimsContext(ctx context.Context, claims jwt.Claims) context.Context { 37 | return context.WithValue(ctx, claimsKey, claims) 38 | } 39 | 40 | // FromClaimsContext returns the AuthRecord value stored in ctx, if any. 41 | func FromClaimsContext(ctx context.Context) (jwt.Claims, bool) { 42 | claims, ok := ctx.Value(claimsKey).(jwt.Claims) 43 | return claims, ok 44 | } 45 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | // Package konnect is a Go implementation of an OpenID Connect server with 19 | // flexibale authorization and authentication backends and consent screen. 20 | // 21 | // See README.md for more info. 22 | package konnect // import "stash.kopano.io/kc/konnect" 23 | -------------------------------------------------------------------------------- /encryption/encryption.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package encryption 19 | 20 | import ( 21 | "fmt" 22 | 23 | "golang.org/x/crypto/nacl/secretbox" 24 | ) 25 | 26 | const ( 27 | // KeySize is the size of the keys created by GenerateKey() 28 | KeySize = 32 29 | // NonceSize is the size of the nonces created by GenerateNonce() 30 | NonceSize = 24 31 | ) 32 | 33 | // Encrypt generates a random nonce and encrypts the input using nacl.secretbox 34 | // package. We store the nonce in the first 24 bytes of the encrypted text. 35 | func Encrypt(msg []byte, key *[KeySize]byte) ([]byte, error) { 36 | nonce, err := GenerateNonce() 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return encryptWithNonce(msg, nonce, key) 42 | } 43 | 44 | func encryptWithNonce(msg []byte, nonce *[NonceSize]byte, key *[KeySize]byte) ([]byte, error) { 45 | encrypted := secretbox.Seal(nonce[:], msg, nonce, key) 46 | return encrypted, nil 47 | } 48 | 49 | // Decrypt extracts the nonce from the encrypted text, and attempts to decrypt 50 | // with nacl.box. 51 | func Decrypt(msg []byte, key *[KeySize]byte) ([]byte, error) { 52 | if len(msg) < (NonceSize + secretbox.Overhead) { 53 | return nil, fmt.Errorf("wrong length of ciphertext") 54 | } 55 | 56 | var nonce [NonceSize]byte 57 | copy(nonce[:], msg[:NonceSize]) 58 | decrypted, ok := secretbox.Open(nil, msg[NonceSize:], &nonce, key) 59 | if !ok { 60 | return nil, fmt.Errorf("decryption failed") 61 | } 62 | 63 | return decrypted, nil 64 | } 65 | -------------------------------------------------------------------------------- /encryption/encryption_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package encryption 19 | 20 | import ( 21 | "bytes" 22 | "encoding/hex" 23 | "testing" 24 | ) 25 | 26 | var ( 27 | defaultSecretKey [KeySize]byte 28 | defaultNonce [NonceSize]byte 29 | defaultPlainText []byte 30 | defaultEncryptedText []byte 31 | ) 32 | 33 | func init() { 34 | secretKey, _ := hex.DecodeString("0f9c41164bab0d26fd4e1d50ea7323b294a5cd4c5bc79dbfd667b0d870bdd11e") 35 | copy(defaultSecretKey[:], secretKey[:KeySize]) 36 | 37 | nonce, _ := hex.DecodeString("87e0e8c89c58eda1ced9aa829e1f32d8837c5f9660717a49") 38 | copy(defaultNonce[:], nonce[:NonceSize]) 39 | 40 | defaultPlainText = []byte("We are Kopano, and we empower you to have choice to use multiple ways to communicate with others, be it email, video meetings or chat.") 41 | 42 | defaultEncryptedText, _ = hex.DecodeString("87e0e8c89c58eda1ced9aa829e1f32d8837c5f9660717a4947526753dbe3a493d2c97961591c0d05dd7525ca34165ac1aeb0950789cfd3f8a8c8e981f564494c0a8cb039bf14d844af6dfb107af432160d58e3ff544f28c00f1b27ebcc12424b19bc2325fdb31513da4b45731adc004b05b3fe821038f77097bffb4507e9d7a79b86cb1318e1bfe4ab7a84d01888daa916470238fe71d7d83c16fd6066d672cd491b29f590fcfada5a0c89386d31") 43 | } 44 | 45 | func TestGenerateNonce(t *testing.T) { 46 | nonce, err := GenerateNonce() 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | if len(nonce) != NonceSize { 52 | t.Fatalf("nonce has wrong size: got %v want %v", len(nonce), NonceSize) 53 | } 54 | } 55 | 56 | func TestGenerateKey(t *testing.T) { 57 | secretKey, err := GenerateKey() 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | if len(secretKey) != KeySize { 63 | t.Fatalf("secret key has wrong size: got %v want %v", len(secretKey), KeySize) 64 | } 65 | } 66 | 67 | func TestEncryptWithNonce(t *testing.T) { 68 | msg := []byte(defaultPlainText) 69 | encrypted, err := encryptWithNonce(msg, &defaultNonce, &defaultSecretKey) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | if !bytes.Equal(encrypted, defaultEncryptedText) { 75 | t.Fatalf("encrypted text does not match expected value, %x", encrypted) 76 | } 77 | } 78 | 79 | func TestEncrypt(t *testing.T) { 80 | msg := []byte(defaultPlainText) 81 | encrypted, err := Encrypt(msg, &defaultSecretKey) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | if bytes.Equal(encrypted, defaultEncryptedText) { 87 | t.Fatal("encrypted text does not seem to have a nonce") 88 | } 89 | } 90 | 91 | func TestDecrypt(t *testing.T) { 92 | decrypted, err := Decrypt(defaultEncryptedText, &defaultSecretKey) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | if !bytes.Equal(decrypted, defaultPlainText) { 98 | t.Fatalf("decrypted text does not match expected value, got %v", decrypted) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /encryption/generate.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package encryption 19 | 20 | import ( 21 | "crypto/rand" 22 | "io" 23 | ) 24 | 25 | // GenerateKey generates a new random secret key. 26 | func GenerateKey() (*[KeySize]byte, error) { 27 | key := new([KeySize]byte) 28 | _, err := io.ReadFull(rand.Reader, key[:]) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return key, nil 34 | } 35 | 36 | // GenerateNonce creates a new random nonce. 37 | func GenerateNonce() (*[NonceSize]byte, error) { 38 | nonce := new([NonceSize]byte) 39 | _, err := io.ReadFull(rand.Reader, nonce[:]) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return nonce, nil 45 | } 46 | -------------------------------------------------------------------------------- /examples/coookieserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from http.server import BaseHTTPRequestHandler, HTTPServer 4 | from http import cookies 5 | import json 6 | 7 | # User table. 8 | users = {} 9 | 10 | 11 | def addUsers(l): 12 | for u in l: 13 | users[u.get("sub")] = u 14 | 15 | # Add numeric user ID. 16 | u["id"] = len(users) 17 | 18 | 19 | addUsers([ 20 | { 21 | "sub": "arthur", 22 | "name": "Arthur Dent", 23 | "email": "arthur@earth.local", 24 | }, 25 | { 26 | "sub": "trillian", 27 | "name": "Trillian", 28 | "email": "trillian@galaxy.local", 29 | }, 30 | { 31 | "sub": "ford", 32 | "name": "Ford Prefect", 33 | "email": "ford@betelgeuse.local", 34 | } 35 | ]) 36 | 37 | 38 | class Handler(BaseHTTPRequestHandler): 39 | def do_POST(self): 40 | cookie = cookies.SimpleCookie() 41 | cookie.load(self.headers.get("Cookie", "")) 42 | id = cookie.get("minioidc-simple", None) 43 | if id is None: 44 | self.send_response(401) 45 | self.end_headers() 46 | return 47 | 48 | user = users.get(id.value, None) 49 | if user is None: 50 | self.send_response(403) 51 | self.end_headers() 52 | return 53 | 54 | self.send_response(202) 55 | self.send_header('Content-Type', 'application/json') 56 | self.end_headers() 57 | 58 | self.wfile.write(json.dumps(user, indent=4, sort_keys=True).encode()) 59 | return 60 | 61 | 62 | def run(server_class=HTTPServer, handler_class=Handler, port=8080): 63 | address = ('127.0.0.1', port) 64 | server = server_class(address, handler_class) 65 | print('Starting HTTP server ...') 66 | server.serve_forever() 67 | 68 | 69 | if __name__ == '__main__': 70 | from sys import argv 71 | 72 | if len(argv) == 2: 73 | run(port=int(argv[1])) 74 | else: 75 | run() 76 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module stash.kopano.io/kc/konnect 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/beevik/etree v1.1.0 7 | github.com/crewjam/httperr v0.2.0 8 | github.com/crewjam/saml v0.4.3 9 | github.com/deckarep/golang-set v1.7.1 10 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 11 | github.com/ghodss/yaml v1.0.0 12 | github.com/go-asn1-ber/asn1-ber v1.4.1 // indirect 13 | github.com/go-ldap/ldap/v3 v3.1.7 14 | github.com/golang/protobuf v1.3.4 // indirect 15 | github.com/google/go-querystring v1.0.0 16 | github.com/gorilla/mux v1.7.4 17 | github.com/gorilla/schema v1.1.0 18 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 19 | github.com/longsleep/go-metrics v0.0.0-20191013204616-cddea569b0ea 20 | github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 21 | github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 22 | github.com/pkg/errors v0.9.1 // indirect 23 | github.com/prometheus/client_golang v1.5.0 24 | github.com/prometheus/procfs v0.0.10 // indirect 25 | github.com/rs/cors v1.7.0 26 | github.com/russellhaering/goxmldsig v1.1.0 27 | github.com/satori/go.uuid v1.2.0 28 | github.com/sirupsen/logrus v1.4.2 29 | github.com/spf13/cobra v0.0.6 30 | github.com/spf13/pflag v1.0.5 // indirect 31 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 32 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a 33 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect 34 | golang.org/x/text v0.3.2 // indirect 35 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 36 | gopkg.in/square/go-jose.v2 v2.4.1 37 | gopkg.in/yaml.v2 v2.2.8 38 | stash.kopano.io/kgol/kcc-go/v5 v5.0.1 39 | stash.kopano.io/kgol/ksurveyclient-go v0.6.0 40 | stash.kopano.io/kgol/oidc-go v0.3.1 41 | stash.kopano.io/kgol/rndm v1.1.0 42 | ) 43 | 44 | replace github.com/crewjam/httperr => github.com/crewjam/httperr v0.2.0 45 | 46 | replace github.com/mattermost/xml-roundtrip-validator => github.com/mattermost/xml-roundtrip-validator v0.0.0-20201213122252-bcd7e1b9601e 47 | -------------------------------------------------------------------------------- /identifier-registration.yaml.in: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # OpenID Connect client registry. 4 | clients: 5 | # - id: playground.js 6 | # name: OIDC Playground 7 | # application_type: web 8 | # redirect_uris: 9 | # - https://my-host:8509/ 10 | # origins: 11 | # - https://my-host:8509 12 | 13 | # - id: playground-trusted.js 14 | # name: Trusted OIDC Playground 15 | # trusted: yes 16 | # application_type: web 17 | # redirect_uris: 18 | # - https://my-host:8509/ 19 | # origins: 20 | # - https://my-host:8509 21 | 22 | # - id: playground-trusted.js 23 | # name: Trusted Insecure OIDC Playground 24 | # trusted: yes 25 | # application_type: web 26 | # insecure: yes 27 | 28 | # - id: client-with-keys 29 | # secret: super 30 | # application_type: native 31 | # redirect_uris: 32 | # - http://localhost 33 | # trusted_scopes: 34 | # - konnect/guestok 35 | # - kopano/kwm 36 | # jwks: 37 | # keys: 38 | # - kty: EC 39 | # use: sig 40 | # kid: client-with-keys-key-1 41 | # crv: P-256 42 | # x: RTZpWoRbjwX1YavmSHVBj6Cy3Yzdkkp6QLvTGB22D0c 43 | # y: jeavjwcX0xlDSchFcBMzXSU7wGs2VPpNxWCwmxFvmF0 44 | # request_object_signing_alg: ES256 45 | 46 | # - id: first 47 | # secret: lala 48 | # application_type: native 49 | # redirect_uris: 50 | # - my://app 51 | 52 | # - id: second 53 | # secret: lulu 54 | # application_type: native 55 | # redirect_uris: 56 | # - http://localhost 57 | 58 | # External authority registry. 59 | authorities: 60 | # - id: my-univention-oidc 61 | # name: Univention 62 | # client_id: kopano-konnect 63 | # authority_type: oidc 64 | # jwks: 65 | # keys: 66 | # - kty: EC 67 | # use: sig 68 | # kid: example-key-1 69 | # crv: P-256 70 | # x: RTZpWoRbjwX1YavmSHVBj6Cy3Yzdkkp6QLvTGB22D0c 71 | # y: jeavjwcX0xlDSchFcBMzXSU7wGs2VPpNxWCwmxFvmF0 72 | # default: yes 73 | # authorization_endpoint: https://my-univention/signin/v1/identifier/_/authorize 74 | # response_type: id_token 75 | # scopes: 76 | # - openid 77 | # - profile 78 | # identity_claim_name: preferred_username 79 | # identity_aliases: 80 | # external-user-a: local-user-a 81 | # external-user-b: local-user-b 82 | # identity_alias_required: true 83 | 84 | # - id: my-univention-saml2 85 | # name: Univention 86 | # entity_id: kopano-konnect 87 | # authority_type: saml2 88 | # default: yes 89 | # trusted: yes 90 | # discover: yes 91 | # metadata_endpoint: https://my-univention/simplesamlphp/saml2/idp/metadata.php 92 | # identity_claim_name: uid 93 | # identity_alias_required: false 94 | # end_session_enabled: true 95 | -------------------------------------------------------------------------------- /identifier/.env: -------------------------------------------------------------------------------- 1 | PORT=3001 2 | HOST=127.0.0.1 3 | BROWSER=none 4 | REACT_APP_KOPANO_BUILD=0.0.0-dev-env 5 | INLINE_RUNTIME_CHUNK=false 6 | EXTEND_ESLINT=true 7 | -------------------------------------------------------------------------------- /identifier/.eslintignore: -------------------------------------------------------------------------------- 1 | build/* 2 | node_modules/* 3 | 4 | -------------------------------------------------------------------------------- /identifier/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["react-intl-format"], 3 | "extends": ["plugin:react/recommended", "plugin:jest/recommended"], 4 | "settings": { 5 | "react": { 6 | "version": "detect" 7 | } 8 | }, 9 | "parser": "babel-eslint", 10 | "rules": { 11 | "react-intl-format/missing-formatted-message": [ 12 | "error", 13 | { 14 | "noTrailingWhitespace": false, 15 | "ignoreLinks": false 16 | } 17 | ], 18 | "react-intl-format/missing-attribute": [ 19 | "error", 20 | { 21 | "noTrailingWhitespace": false, 22 | "noSpreadOperator": true 23 | } 24 | ], 25 | "react-intl-format/missing-values": [ 26 | "error" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /identifier/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.yarninstall 6 | 7 | # testing 8 | /coverage 9 | .eslintcache 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # i18n 26 | *.mo 27 | /src/locales/*.json 28 | /i18n/src 29 | -------------------------------------------------------------------------------- /identifier/Makefile: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | YARN ?= yarn 4 | 5 | # Variables 6 | 7 | VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2>/dev/null | sed 's/^v//' || \ 8 | cat $(CURDIR)/../.version 2> /dev/null || echo 0.0.0-unreleased) 9 | 10 | # Build 11 | 12 | .PHONY: all 13 | all: build 14 | 15 | .PHONY: build 16 | build: vendor | src i18n ; $(info building identifier Webapp ...) @ 17 | @rm -rf build 18 | 19 | REACT_APP_KOPANO_BUILD="${VERSION}" $(YARN) run build 20 | 21 | .PHONY: src 22 | src: 23 | @$(MAKE) -C src 24 | 25 | .PHONY: i18n 26 | i18n: vendor 27 | @$(MAKE) -C i18n 28 | 29 | .PHONY: lint 30 | lint: vendor ; $(info running eslint ...) @ 31 | @$(YARN) eslint . --cache && echo "eslint: no lint errors" 32 | 33 | .PHONY: lint-checkstyle 34 | lint-checkstyle: vendor ; $(info running eslint checkstyle ...) @ 35 | @mkdir -p ../test 36 | $(YARN) eslint -f checkstyle -o ../test/tests.eslint.xml . || true 37 | 38 | # Yarn 39 | 40 | .PHONY: vendor 41 | vendor: .yarninstall 42 | 43 | .yarninstall: package.json ; $(info getting depdencies with yarn ...) @ 44 | @$(YARN) install --silent 45 | @touch $@ 46 | 47 | # Stuff 48 | 49 | .PHONY: licenses 50 | licenses: 51 | echo "## Konnect identifier webapp\n" 52 | @$(YARN) run -s licenses 53 | 54 | .PHONY: clean ; $(info cleaning identifier Webapp ...) @ 55 | clean: 56 | $(YARN) cache clean 57 | @rm -rf build 58 | @rm -rf node_modules 59 | @rm -f .yarninstall 60 | 61 | @$(MAKE) -C src clean 62 | 63 | .PHONY: version 64 | version: 65 | @echo $(VERSION) 66 | -------------------------------------------------------------------------------- /identifier/README.md: -------------------------------------------------------------------------------- 1 | # Kopano Konnect Identifier 2 | 3 | Web app for browser sign-in, sign-out and account management. 4 | -------------------------------------------------------------------------------- /identifier/backends/backend.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package backends 19 | 20 | import ( 21 | "context" 22 | 23 | "stash.kopano.io/kc/konnect/identifier/meta/scopes" 24 | "stash.kopano.io/kc/konnect/identity" 25 | ) 26 | 27 | // A Backend is an identifier Backend providing functionality to logon and to 28 | // fetch user meta data. 29 | type Backend interface { 30 | RunWithContext(context.Context) error 31 | 32 | Logon(ctx context.Context, audience string, username string, password string) (success bool, userID *string, sessionRef *string, user UserFromBackend, err error) 33 | GetUser(ctx context.Context, userID string, sessionRef *string) (user UserFromBackend, err error) 34 | 35 | ResolveUserByUsername(ctx context.Context, username string) (user UserFromBackend, err error) 36 | 37 | RefreshSession(ctx context.Context, userID string, sessionRef *string, claims map[string]interface{}) error 38 | DestroySession(ctx context.Context, sessionRef *string) error 39 | 40 | UserClaims(userID string, authorizedScopes map[string]bool) map[string]interface{} 41 | ScopesSupported() []string 42 | ScopesMeta() *scopes.Scopes 43 | 44 | Name() string 45 | } 46 | 47 | // UserFromBackend are users as provided by backends which can have additional 48 | // claims together with a user name. 49 | type UserFromBackend interface { 50 | identity.UserWithUsername 51 | BackendClaims() map[string]interface{} 52 | } 53 | -------------------------------------------------------------------------------- /identifier/backends/kc/consts.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package kc 19 | 20 | // Define scopes supported by KC. 21 | const ( 22 | ScopeKopanoGC = "kopano/gc" 23 | ) 24 | 25 | // Define claims supported by KC. 26 | const ( 27 | KopanoGCIDClaim = "kopano/gc/id" 28 | ) 29 | -------------------------------------------------------------------------------- /identifier/backends/ldap/const.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package ldap 19 | 20 | // Define some known LDAP attribute descriptors. 21 | const ( 22 | AttributeDN = "dn" 23 | AttributeLogin = "uid" 24 | AttributeEmail = "mail" 25 | AttributeName = "cn" 26 | AttributeFamilyName = "sn" 27 | AttributeGivenName = "givenName" 28 | AttributeUUID = "uuid" 29 | ) 30 | 31 | // Additional mappable virtual attributes. 32 | const ( 33 | AttributeNumericUID = "konnectNumericID" 34 | ) 35 | 36 | // Define our known LDAP attribute value types. 37 | const ( 38 | AttributeValueTypeText = "text" 39 | AttributeValueTypeBinary = "binary" 40 | AttributeValueTypeUUID = "uuid" 41 | ) 42 | -------------------------------------------------------------------------------- /identifier/claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identifier 19 | 20 | // Additional claims as used by the identifier in its own tokens. 21 | const ( 22 | SessionIDClaim = "sid" 23 | LogonRefClaim = "lref" 24 | ExternalAuthorityIDClaim = "eaid" 25 | ) 26 | 27 | // History claims previously used by the identifier in its own tokens. 28 | const ( 29 | ObsoleteUserClaimsClaim = "claims" 30 | ) 31 | -------------------------------------------------------------------------------- /identifier/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identifier 19 | 20 | import ( 21 | "net/url" 22 | 23 | "stash.kopano.io/kc/konnect/config" 24 | "stash.kopano.io/kc/konnect/identifier/backends" 25 | ) 26 | 27 | // Config defines a Server's configuration settings. 28 | type Config struct { 29 | Config *config.Config 30 | 31 | BaseURI *url.URL 32 | LogonCookieName string 33 | ScopesConf string 34 | 35 | PathPrefix string 36 | StaticFolder string 37 | WebAppDisabled bool 38 | 39 | AuthorizationEndpointURI *url.URL 40 | SignedOutEndpointURI *url.URL 41 | 42 | Backend backends.Backend 43 | } 44 | -------------------------------------------------------------------------------- /identifier/flows.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identifier 19 | 20 | const ( 21 | // FlowOIDC is the string value for the oidc flow. 22 | FlowOIDC = "oidc" 23 | // FlowOAuth is the string value for the oauth flow. 24 | FlowOAuth = "oauth" 25 | // FlowConsent is the string value for the consent flow. 26 | FlowConsent = "consent" 27 | ) 28 | -------------------------------------------------------------------------------- /identifier/i18n/Makefile: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | YARN ?= yarn 4 | MSGMERGE ?= msgmerge 5 | MSGFMT ?= msgfmt 6 | 7 | # Variables 8 | 9 | POT = konnect-identifier.pot 10 | POS = $(wildcard *.po) 11 | 12 | # Translations 13 | 14 | .PHONY: build 15 | build: json 16 | 17 | .PHONY: extract 18 | extract: pot 19 | 20 | .PHONY: i18n/src/messages.json 21 | i18n/src/messages.json: 22 | $(YARN) react-intl-cra './src/**/*.{js,jsx}' -o $@ 23 | 24 | .PHONY: pot 25 | pot: i18n/src/messages.json 26 | $(YARN) rip json2pot './i18n/src/**/*.json' \ 27 | -o ./i18n/$(POT) \ 28 | -c 'id' 29 | 30 | .PHONY: json 31 | json: i18n/src/messages.json 32 | $(YARN) rip po2json './i18n/*.po' \ 33 | -m './i18n/src/**/*.json' \ 34 | -o './src/locales/' \ 35 | -c 'id' \ 36 | --indentation=2 37 | 38 | .PHONY: merge 39 | merge: $(POS) 40 | 41 | .PHONY: stats 42 | stats: 43 | $(foreach po, $(POS), $(shell $(MSGFMT) -v --statistics $(po))) 44 | @- true 45 | 46 | $(POS): FORCE $(POT) 47 | $(MSGMERGE) -U \ 48 | --backup=none \ 49 | --no-wrap \ 50 | $@ $(POT) 51 | 52 | FORCE: 53 | -------------------------------------------------------------------------------- /identifier/meta/meta.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package meta 19 | 20 | import ( 21 | "stash.kopano.io/kc/konnect/identifier/meta/scopes" 22 | ) 23 | 24 | // Meta is a container to hold identifier meta data which can be requested by 25 | // clients. 26 | type Meta struct { 27 | Scopes *scopes.Scopes `json:"scopes"` 28 | } 29 | -------------------------------------------------------------------------------- /identifier/meta/scopes/definition.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package scopes 19 | 20 | // A Definition contains the meta data for a single scope. 21 | type Definition struct { 22 | Priority int `json:"priority" yaml:"priority"` 23 | Description string `json:"description,omitempty" yaml:"description"` 24 | ID string `json:"id,omitempty"` 25 | } 26 | -------------------------------------------------------------------------------- /identifier/modes.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identifier 19 | 20 | const ( 21 | // ModeLogonUsernameEmptyPasswordCookie is the logon mode which requires a 22 | // username which matches the currently signed in user in the cookie and an 23 | // empty password. 24 | ModeLogonUsernameEmptyPasswordCookie = "0" 25 | // ModeLogonUsernamePassword is the logon mode which requires a username 26 | // and a password. 27 | ModeLogonUsernamePassword = "1" 28 | ) 29 | 30 | const ( 31 | // MustBeSignedIn is a authorize mode which tells the authorization code, 32 | // that it is expected to have a signed in user and everything else should 33 | // be treated as error. 34 | MustBeSignedIn = "must" 35 | ) 36 | 37 | const ( 38 | // StateModeEndSession is a state mode which selects end session specific 39 | // actions when processing state requests. 40 | StateModeEndSession = "0" 41 | ) 42 | -------------------------------------------------------------------------------- /identifier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "identifier", 3 | "version": "1.0.0", 4 | "private": true, 5 | "homepage": ".", 6 | "dependencies": { 7 | "@material-ui/core": "^4.8.0", 8 | "@material-ui/icons": "^4.5.0", 9 | "axios": "^0.19.2", 10 | "classnames": "^2.2.6", 11 | "kpop": "https://download.kopano.io/community/kapp:/kpop-2.2.0.tgz", 12 | "query-string": "^5.0.1", 13 | "react": "^16.8.0", 14 | "react-dom": "^16.8.0", 15 | "react-intl": "^2.4.0", 16 | "react-loadable": "^5.3.1", 17 | "react-redux": "^5.0.6", 18 | "react-router": "^5.0.0", 19 | "react-router-dom": "5.0.0", 20 | "react-scripts": "3.3.1", 21 | "redux": "^3.7.2", 22 | "redux-logger": "^3.0.6", 23 | "redux-thunk": "^2.2.0", 24 | "render-if": "^0.1.1", 25 | "typeface-roboto": "^0.0.54" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build && rm -f build/service-worker.js", 30 | "test": "react-scripts test --env=jsdom", 31 | "eject": "react-scripts eject", 32 | "licenses": "NODE_PATH=./node_modules node ../scripts/js-license-ranger.js", 33 | "deduplicate": "yarn-deduplicate -s fewer yarn.lock", 34 | "analyze": "source-map-explorer 'build/static/js/*.js'" 35 | }, 36 | "devDependencies": { 37 | "@babel/runtime": "7.0.0-beta.55", 38 | "eslint": ">=5", 39 | "eslint-plugin-i18n-text": "^1.0.0", 40 | "eslint-plugin-jest": "^22.5.1", 41 | "eslint-plugin-react": "^7.10.0", 42 | "eslint-plugin-react-intl-format": "^1.1.6", 43 | "prop-types": "^15.6.2", 44 | "react-intl-cra": "^0.3.3", 45 | "react-intl-po": "^2.2.2", 46 | "source-map-explorer": "^1.8.0", 47 | "yarn-deduplicate": "^1.1.1" 48 | }, 49 | "resolutions": { 50 | "**/react": "16.8.6", 51 | "**/react-dom": "16.8.6", 52 | "**/@material-ui/core": "4.8.0", 53 | "**/@material-ui/icons": "4.5.1", 54 | "**/react-intl": "2.9.0", 55 | "**/create-react-context": "0.3.0" 56 | }, 57 | "jest": { 58 | "collectCoverageFrom": [ 59 | "src/**/*.js" 60 | ], 61 | "transformIgnorePatterns": [ 62 | "node_modules/?!(kpop)" 63 | ] 64 | }, 65 | "browserslist": [ 66 | ">0.2%", 67 | "not dead", 68 | "not ie <= 11", 69 | "not op_mini all" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /identifier/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Kopano Sign in 10 | 11 | 12 | 15 |
16 |
17 |
18 |
19 |
20 |
aABb
21 | 22 | 23 | -------------------------------------------------------------------------------- /identifier/public/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kopano-dev/konnect/52c7ab10ee1f2a39323b3f9db1514e0433138e2a/identifier/public/static/favicon.ico -------------------------------------------------------------------------------- /identifier/src/Main.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { BrowserRouter } from 'react-router-dom'; 6 | 7 | import { withStyles } from '@material-ui/core/styles'; 8 | 9 | import { enhanceBodyBackground } from './utils'; 10 | import Routes from './Routes'; 11 | 12 | // Trigger loading of background image. 13 | enhanceBodyBackground(); 14 | 15 | const styles = () => ({ 16 | root: { 17 | position: 'relative', 18 | display: 'flex', 19 | flex: 1 20 | } 21 | }); 22 | 23 | class App extends PureComponent { 24 | render() { 25 | const { classes, hello, pathPrefix } = this.props; 26 | 27 | return ( 28 |
29 | 30 | 31 | 32 |
33 | ); 34 | } 35 | 36 | reload(event) { 37 | event.preventDefault(); 38 | 39 | window.location.reload(); 40 | } 41 | } 42 | 43 | App.propTypes = { 44 | classes: PropTypes.object.isRequired, 45 | 46 | hello: PropTypes.object, 47 | updateAvailable: PropTypes.bool.isRequired, 48 | pathPrefix: PropTypes.string.isRequired 49 | }; 50 | 51 | const mapStateToProps = (state) => { 52 | const { hello, updateAvailable, pathPrefix } = state.common; 53 | 54 | return { 55 | hello, 56 | updateAvailable, 57 | pathPrefix 58 | }; 59 | }; 60 | 61 | export default connect(mapStateToProps)(withStyles(styles)(App)); 62 | -------------------------------------------------------------------------------- /identifier/src/Main.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { Provider } from 'react-redux'; 5 | 6 | import store from './store'; 7 | import App from './Main'; 8 | 9 | it('renders without crashing', () => { 10 | const div = document.createElement('div'); 11 | ReactDOM.render(, div); 12 | }); 13 | -------------------------------------------------------------------------------- /identifier/src/Makefile: -------------------------------------------------------------------------------- 1 | all: images 2 | 3 | .PHONY: images 4 | images: 5 | @$(MAKE) -C images 6 | 7 | clean: 8 | @$(MAKE) -C images clean 9 | -------------------------------------------------------------------------------- /identifier/src/Routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Route, Switch } from 'react-router-dom'; 5 | import AsyncComponent from 'kpop/es/AsyncComponent'; 6 | 7 | import PrivateRoute from './components/PrivateRoute'; 8 | 9 | const AsyncLogin = AsyncComponent(() => 10 | import(/* webpackChunkName: "containers-login" */ './containers/Login')); 11 | const AsyncWelcome = AsyncComponent(() => 12 | import(/* webpackChunkName: "containers-welcome" */ './containers/Welcome')); 13 | const AsyncGoodbye = AsyncComponent(() => 14 | import(/* webpackChunkName: "containers-goodbye" */ './containers/Goodbye')); 15 | 16 | const Routes = ({ hello }) => ( 17 | 18 | 24 | 29 | 33 | 34 | ); 35 | 36 | Routes.propTypes = { 37 | hello: PropTypes.object 38 | }; 39 | 40 | export default Routes; 41 | -------------------------------------------------------------------------------- /identifier/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_ERROR = 'RECEIVE_ERROR'; 2 | 3 | export const RESET_HELLO = 'RESET_HELLO'; 4 | export const EXECUTE_HELLO = 'EXECUTE_HELLO'; 5 | export const RECEIVE_HELLO = 'RECEIVE_HELLO'; 6 | 7 | export const RECEIVE_VALIDATE_LOGON = 'RECEIVE_VALIDATE_LOGON'; 8 | export const REQUEST_LOGON = 'REQUEST_LOGON'; 9 | export const EXECUTE_LOGON = 'EXECUTE_LOGON'; 10 | export const RECEIVE_LOGON = 'RECEIVE_LOGON'; 11 | export const UPDATE_INPUT = 'UPDATE_INPUT'; 12 | 13 | export const REQUEST_CONSENT_ALLOW = 'REQUEST_CONSENT_ALLOW'; 14 | export const REQUEST_CONSENT_CANCEL = 'REQUEST_CONSENT_CANCEL'; 15 | export const EXECUTE_CONSENT = 'EXECUTE_CONSENT'; 16 | export const RECEIVE_CONSENT = 'RECEIVE_CONSENT'; 17 | 18 | export const REQUEST_LOGOFF = 'REQUEST_LOGOFF'; 19 | export const EXECUTE_LOGOFF = 'EXECUTE_LOGOFF'; 20 | export const RECEIVE_LOGOFF = 'RECEIVE_LOGOFF'; 21 | 22 | export const SERVICE_WORKER_NEW_CONTENT = 'SERVICE_WORKER_NEW_CONTENT'; 23 | export const SERVICE_WORKER_READY = 'SERVICE_WORKER_READY'; 24 | export const SERVICE_WORKER_ERROR = 'SERVICE_WORKER_ERROR'; 25 | export const SERVICE_WORKER_OFFLINE = 'SERVICE_WORKER_OFFLINE'; 26 | -------------------------------------------------------------------------------- /identifier/src/actions/utils.js: -------------------------------------------------------------------------------- 1 | import { 2 | ExtendedError, 3 | ERROR_HTTP_NETWORK_ERROR, 4 | ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS 5 | } from '../errors'; 6 | 7 | export function handleAxiosError(error) { 8 | if (error.request) { 9 | // Axios errors. 10 | if (error.response) { 11 | error = new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, error.response); 12 | } else { 13 | error = new ExtendedError(ERROR_HTTP_NETWORK_ERROR); 14 | } 15 | } 16 | 17 | return error; 18 | } 19 | -------------------------------------------------------------------------------- /identifier/src/app.css: -------------------------------------------------------------------------------- 1 | /* additional css on top of kpop */ 2 | -------------------------------------------------------------------------------- /identifier/src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Loadable from 'react-loadable'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import { MuiThemeProvider } from '@material-ui/core/styles'; 7 | 8 | import { defaultTheme as theme } from 'kpop/es/theme'; 9 | import IntlContainer from 'kpop/es/IntlContainer'; 10 | import Loading from 'kpop/es/Loading'; 11 | import { unregister } from 'kpop/es/serviceWorker'; 12 | 13 | import store from './store'; 14 | import translations from './locales'; 15 | 16 | const onLocaleChanged = async locale => { 17 | console.info('locale', locale); // eslint-disable-line no-console 18 | }; 19 | 20 | // NOTE(longsleep): Load async with loader, this enables code splitting via Webpack. 21 | const LoadableApp = Loadable({ 22 | loader: () => import(/* webpackChunkName: "identifier-main" */ './Main'), 23 | loading: Loading, 24 | timeout: 20000 25 | }); 26 | 27 | ReactDOM.render( 28 | 29 | 30 | 31 | 32 | 33 | 34 | , 35 | document.getElementById('root') 36 | ); 37 | 38 | unregister(); 39 | -------------------------------------------------------------------------------- /identifier/src/components/ClientDisplayName.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const ClientDisplayName = ({ client, ...rest }) => ( 5 | {client.display_name ? client.display_name : client.id} 6 | ); 7 | 8 | ClientDisplayName.propTypes = { 9 | client: PropTypes.object.isRequired 10 | }; 11 | 12 | export default ClientDisplayName; 13 | -------------------------------------------------------------------------------- /identifier/src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { FormattedMessage } from 'react-intl'; 6 | 7 | import { withStyles } from '@material-ui/core/styles'; 8 | import LinearProgress from '@material-ui/core/LinearProgress'; 9 | import Grid from '@material-ui/core/Grid'; 10 | import Typography from '@material-ui/core/Typography'; 11 | import Button from '@material-ui/core/Button'; 12 | import renderIf from 'render-if'; 13 | 14 | import { retryHello } from '../actions/common'; 15 | import { ErrorMessage } from '../errors'; 16 | 17 | const styles = theme => ({ 18 | root: { 19 | flexGrow: 1, 20 | position: 'absolute', 21 | top: 0, 22 | bottom: 0, 23 | left: 0, 24 | right: 0 25 | }, 26 | progress: { 27 | height: '4px', 28 | width: '100px' 29 | }, 30 | button: { 31 | marginTop: theme.spacing(5) 32 | } 33 | }); 34 | 35 | class Loading extends React.PureComponent { 36 | render() { 37 | const { classes, error } = this.props; 38 | 39 | return ( 40 | 41 | 42 | {renderIf(error === null)(() => ( 43 | 44 | ))} 45 | {renderIf(error !== null)(() => ( 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 63 |
64 | ))} 65 |
66 |
67 | ); 68 | } 69 | 70 | retry(event) { 71 | event.preventDefault(); 72 | 73 | this.props.dispatch(retryHello()); 74 | } 75 | } 76 | 77 | Loading.propTypes = { 78 | classes: PropTypes.object.isRequired, 79 | 80 | error: PropTypes.object, 81 | 82 | dispatch: PropTypes.func.isRequired 83 | }; 84 | 85 | const mapStateToProps = (state) => { 86 | const { error } = state.common; 87 | 88 | return { 89 | error 90 | }; 91 | }; 92 | 93 | export default connect(mapStateToProps)(withStyles(styles)(Loading)); 94 | -------------------------------------------------------------------------------- /identifier/src/components/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route } from 'react-router-dom'; 4 | 5 | import RedirectWithQuery from './RedirectWithQuery'; 6 | 7 | const PrivateRoute = ({ component: Target, hello, ...rest }) => ( 8 | ( 9 | hello ? ( 10 | 11 | ) : ( 12 | 13 | ) 14 | )}/> 15 | ); 16 | 17 | PrivateRoute.propTypes = { 18 | component: PropTypes.func.isRequired, 19 | hello: PropTypes.object 20 | }; 21 | 22 | export default PrivateRoute; 23 | -------------------------------------------------------------------------------- /identifier/src/components/RedirectWithQuery.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withRouter } from 'react-router'; 4 | import { Redirect } from 'react-router-dom'; 5 | 6 | const RedirectWithQuery = ({target, location, ...rest}) => { 7 | const to = { 8 | pathname: target, 9 | search: location.search, 10 | hash: location.hash 11 | }; 12 | 13 | return ( 14 | 15 | ); 16 | }; 17 | 18 | RedirectWithQuery.propTypes = { 19 | target: PropTypes.string.isRequired, 20 | location: PropTypes.object.isRequired 21 | }; 22 | 23 | export default withRouter(RedirectWithQuery); 24 | -------------------------------------------------------------------------------- /identifier/src/components/ResponsiveDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Dialog from '@material-ui/core/Dialog'; 5 | import withMobileDialog from '@material-ui/core/withMobileDialog'; 6 | 7 | const ResponsiveDialog = (props) => { 8 | return ; 9 | }; 10 | 11 | ResponsiveDialog.propTypes = { 12 | fullScreen: PropTypes.bool.isRequired 13 | }; 14 | 15 | export default withMobileDialog()(ResponsiveDialog); 16 | -------------------------------------------------------------------------------- /identifier/src/components/ResponsiveScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import DialogContent from '@material-ui/core/DialogContent'; 8 | import DialogActions from '@material-ui/core/DialogActions'; 9 | import LocaleSelect from 'kpop/es/IntlContainer/LocaleSelect'; 10 | 11 | import ResponsiveDialog from './ResponsiveDialog'; 12 | import KopanoLogo from '../images/kopano-logo.svg'; 13 | import Loading from './Loading'; 14 | 15 | const styles = theme => ({ 16 | root: { 17 | display: 'flex', 18 | flex: 1 19 | }, 20 | content: { 21 | paddingTop: 24, 22 | paddingBottom: 12, 23 | minHeight: 500, 24 | paddingLeft: theme.spacing(2), 25 | paddingRight: theme.spacing(2), 26 | position: 'relative' 27 | }, 28 | logo: { 29 | height: 18, 30 | marginBottom: theme.spacing(2) 31 | }, 32 | actions: { 33 | marginTop: -40, 34 | justifyContent: 'flex-start', 35 | paddingLeft: theme.spacing(3), 36 | paddingRight: theme.spacing(3) 37 | } 38 | }); 39 | 40 | const ResponsiveScreen = (props) => { 41 | const { 42 | classes, 43 | withoutLogo, 44 | withoutPadding, 45 | loading, 46 | children, 47 | className, 48 | DialogProps, 49 | PaperProps, 50 | ...other 51 | } = props; 52 | 53 | const logo = withoutLogo ? null : 54 | Kopano; 55 | 56 | const content = loading ? : (withoutPadding ? children : {children}); 57 | 58 | return ( 59 | 61 | 66 |
67 | {logo} 68 | {content} 69 |
70 | 71 |
72 |
73 | ); 74 | }; 75 | 76 | ResponsiveScreen.defaultProps = { 77 | withoutLogo: false, 78 | withoutPadding: false, 79 | loading: false 80 | }; 81 | 82 | ResponsiveScreen.propTypes = { 83 | classes: PropTypes.object.isRequired, 84 | withoutLogo: PropTypes.bool, 85 | withoutPadding: PropTypes.bool, 86 | loading: PropTypes.bool, 87 | children: PropTypes.node.isRequired, 88 | className: PropTypes.string, 89 | PaperProps: PropTypes.object, 90 | DialogProps: PropTypes.object 91 | }; 92 | 93 | export default withStyles(styles)(ResponsiveScreen); 94 | -------------------------------------------------------------------------------- /identifier/src/components/ScopesList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import List from '@material-ui/core/List'; 3 | import ListItem from '@material-ui/core/ListItem'; 4 | import ListItemText from '@material-ui/core/ListItemText'; 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import PropTypes from 'prop-types'; 7 | import Checkbox from '@material-ui/core/Checkbox'; 8 | 9 | import { injectIntl, intlShape, defineMessages, FormattedMessage } from 'react-intl'; 10 | 11 | const styles = () => ({ 12 | row: { 13 | paddingTop: 0, 14 | paddingBottom: 0 15 | } 16 | }); 17 | 18 | const scopeIDTranslations = defineMessages({ 19 | 'scope_alias_basic': { 20 | id: 'konnect.scopeDescription.aliasBasic', 21 | defaultMessage: 'Access your basic account information' 22 | }, 23 | 'scope_offline_access': { 24 | id: 'konnect.scopeDescription.offlineAccess', 25 | defaultMessage: 'Keep the allowed access persistently and forever' 26 | } 27 | }); 28 | 29 | const ScopesList = ({scopes, meta, classes, intl, ...rest}) => { 30 | const { mapping, definitions } = meta; 31 | 32 | const rows = []; 33 | const known = {}; 34 | 35 | // TODO(longsleep): Sort scopes according to priority. 36 | for (let scope in scopes) { 37 | if (!scopes[scope]) { 38 | continue; 39 | } 40 | let id = mapping[scope]; 41 | if (id) { 42 | if (known[id]) { 43 | continue; 44 | } 45 | known[id] = true; 46 | } else { 47 | id = scope; 48 | } 49 | let definition = definitions[id]; 50 | let label ; 51 | if (definition) { 52 | if (definition.id) { 53 | const translation = scopeIDTranslations[definition.id]; 54 | if (translation) { 55 | label = intl.formatMessage(translation); 56 | } 57 | } 58 | if (!label) { 59 | label = definition.description; 60 | } 61 | } 62 | if (!label) { 63 | label = ; 68 | } 69 | 70 | rows.push( 71 | 81 | 82 | 83 | ); 84 | } 85 | 86 | return ( 87 | 88 | {rows} 89 | 90 | ); 91 | }; 92 | 93 | ScopesList.propTypes = { 94 | classes: PropTypes.object.isRequired, 95 | intl: intlShape.isRequired, 96 | 97 | scopes: PropTypes.object.isRequired, 98 | meta: PropTypes.object.isRequired 99 | }; 100 | 101 | export default withStyles(styles)(injectIntl(ScopesList)); 102 | -------------------------------------------------------------------------------- /identifier/src/containers/Goodbye/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Goodbyescreen'; 2 | -------------------------------------------------------------------------------- /identifier/src/containers/Login/Loginscreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { Route, Switch } from 'react-router-dom'; 6 | 7 | import { withStyles } from '@material-ui/core/styles'; 8 | 9 | import ResponsiveScreen from '../../components/ResponsiveScreen'; 10 | import RedirectWithQuery from '../../components/RedirectWithQuery'; 11 | import { executeHello } from '../../actions/common'; 12 | 13 | import Login from './Login'; 14 | import Chooseaccount from './Chooseaccount'; 15 | import Consent from './Consent'; 16 | 17 | const styles = () => ({ 18 | }); 19 | 20 | class Loginscreen extends React.PureComponent { 21 | componentDidMount() { 22 | this.props.dispatch(executeHello()); 23 | } 24 | 25 | render() { 26 | const { hello } = this.props; 27 | 28 | const loading = hello === null; 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | } 41 | 42 | Loginscreen.propTypes = { 43 | classes: PropTypes.object.isRequired, 44 | 45 | hello: PropTypes.object, 46 | 47 | dispatch: PropTypes.func.isRequired 48 | }; 49 | 50 | const mapStateToProps = (state) => { 51 | const { hello } = state.common; 52 | 53 | return { 54 | hello 55 | }; 56 | }; 57 | 58 | export default connect(mapStateToProps)(withStyles(styles)(Loginscreen)); 59 | -------------------------------------------------------------------------------- /identifier/src/containers/Login/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Loginscreen'; 2 | -------------------------------------------------------------------------------- /identifier/src/containers/Welcome/Welcomescreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { FormattedMessage } from 'react-intl'; 6 | 7 | import { withStyles } from '@material-ui/core/styles'; 8 | import Button from '@material-ui/core/Button'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import DialogActions from '@material-ui/core/DialogActions'; 11 | 12 | import ResponsiveScreen from '../../components/ResponsiveScreen'; 13 | import { executeLogoff } from '../../actions/common'; 14 | 15 | const styles = theme => ({ 16 | button: { 17 | margin: theme.spacing(1), 18 | minWidth: 100 19 | }, 20 | subHeader: { 21 | marginBottom: theme.spacing(5) 22 | } 23 | }); 24 | 25 | class Welcomescreen extends React.PureComponent { 26 | render() { 27 | const { classes, hello } = this.props; 28 | 29 | const loading = hello === null; 30 | return ( 31 | 32 | 33 | 37 | 38 | 39 | 40 | {hello.username} 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 57 | 58 | 59 | ); 60 | } 61 | 62 | logoff(event) { 63 | event.preventDefault(); 64 | 65 | this.props.dispatch(executeLogoff()).then((response) => { 66 | const { history } = this.props; 67 | 68 | if (response.success) { 69 | history.push('/identifier'); 70 | } 71 | }); 72 | } 73 | } 74 | 75 | Welcomescreen.propTypes = { 76 | classes: PropTypes.object.isRequired, 77 | 78 | hello: PropTypes.object, 79 | 80 | dispatch: PropTypes.func.isRequired, 81 | history: PropTypes.object.isRequired 82 | }; 83 | 84 | const mapStateToProps = (state) => { 85 | const { hello } = state.common; 86 | 87 | return { 88 | hello 89 | }; 90 | }; 91 | 92 | export default connect(mapStateToProps)(withStyles(styles)(Welcomescreen)); 93 | -------------------------------------------------------------------------------- /identifier/src/containers/Welcome/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Welcomescreen'; 2 | -------------------------------------------------------------------------------- /identifier/src/errors/index.js: -------------------------------------------------------------------------------- 1 | import { injectIntl, defineMessages } from 'react-intl'; 2 | 3 | export const ERROR_LOGIN_VALIDATE_MISSINGUSERNAME = 'konnect.error.login.validate.missingUsername'; 4 | export const ERROR_LOGIN_VALIDATE_MISSINGPASSWORD = 'konnect.error.login.validate.missingPassword'; 5 | export const ERROR_LOGIN_FAILED = 'konnect.error.login.failed'; 6 | export const ERROR_HTTP_NETWORK_ERROR = 'konnet.error.http.networkError'; 7 | export const ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS = 'konnect.error.http.unexpectedResponseStatus'; 8 | export const ERROR_HTTP_UNEXPECTED_RESPONSE_STATE = 'konnect.error.http.unexpectedResponseState'; 9 | 10 | // Translatable error messages. 11 | const translations = defineMessages({ 12 | [ERROR_LOGIN_VALIDATE_MISSINGUSERNAME]: { 13 | id: ERROR_LOGIN_VALIDATE_MISSINGUSERNAME, 14 | defaultMessage: 'Enter an username' 15 | }, 16 | [ERROR_LOGIN_VALIDATE_MISSINGPASSWORD]: { 17 | id: ERROR_LOGIN_VALIDATE_MISSINGPASSWORD, 18 | defaultMessage: 'Enter a password' 19 | }, 20 | [ERROR_LOGIN_FAILED]: { 21 | id: ERROR_LOGIN_FAILED, 22 | defaultMessage: 'Logon failed. Please verify your credentials and try again.' 23 | }, 24 | [ERROR_HTTP_NETWORK_ERROR]: { 25 | id: ERROR_HTTP_NETWORK_ERROR, 26 | defaultMessage: 'Network error. Please check your connection and try again.' 27 | }, 28 | [ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS]: { 29 | id: ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, 30 | defaultMessage: 'Unexpected HTTP response: {status}. Please check your connection and try again.' 31 | }, 32 | [ERROR_HTTP_UNEXPECTED_RESPONSE_STATE]: { 33 | id: ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, 34 | defaultMessage: 'Unexpected response state: {state}' 35 | } 36 | }); 37 | 38 | // Error with values. 39 | export class ExtendedError extends Error { 40 | values = undefined; 41 | 42 | constructor(message, values) { 43 | super(message); 44 | if (Error.captureStackTrace !== undefined) { 45 | Error.captureStackTrace(this, ExtendedError); 46 | } 47 | this.values = values; 48 | } 49 | } 50 | 51 | // Component to translate error text with values. 52 | function ErrorMessageComponent(props) { 53 | const { error, intl } = props; 54 | 55 | if (!error) { 56 | return null; 57 | } 58 | 59 | const id = error.id ? error.id : error.message; 60 | const messageDescriptor = Object.assign({}, { 61 | id, 62 | defaultMessage: error.id ? error.message : undefined 63 | }, translations[id]); 64 | 65 | return intl.formatMessage(messageDescriptor, error.values); 66 | } 67 | 68 | export const ErrorMessage = injectIntl(ErrorMessageComponent); 69 | -------------------------------------------------------------------------------- /identifier/src/fancy-background.css: -------------------------------------------------------------------------------- 1 | /* fancy background with two components and fade */ 2 | 3 | #bg { 4 | background-color: white; 5 | } 6 | 7 | #bg > div { 8 | position: absolute; 9 | top: 0; 10 | bottom: 0; 11 | left: 0; 12 | right: 0; 13 | background-size: cover; 14 | background-repeat: no-repeat; 15 | background-position: center; 16 | z-index: 0; 17 | } 18 | 19 | #loader { 20 | /* NOTE(longsleep): White here needed because of the background image */ 21 | color: white; 22 | text-shadow: #000 0px 0px 1px; 23 | } 24 | 25 | /* NOTE(longsleep): This imports the inline image CSS as generated by make. */ 26 | @import url(./images/loginscreen-bg.css); 27 | 28 | #bg-enhanced { 29 | opacity: 0; 30 | transition: opacity 1s; 31 | } 32 | 33 | #bg-enhanced.enhanced { 34 | background-image: url(./images/loginscreen-bg.jpg); 35 | opacity: 1; 36 | } 37 | 38 | #bg-enhanced.enhanced:after { 39 | content: ''; 40 | position: absolute; 41 | top: 0; 42 | bottom: 0; 43 | left: 0; 44 | right: 0; 45 | background-image: url(./images/loginscreen-bg-overlay.svg); 46 | background-size: cover; 47 | background-repeat: no-repeat; 48 | background-position: center; 49 | } 50 | -------------------------------------------------------------------------------- /identifier/src/images/.gitignore: -------------------------------------------------------------------------------- 1 | /loginscreen-bg-thumb.jpg 2 | /loginscreen-bg-thumb.svg 3 | /loginscreen-bg.css 4 | /loginscreen-bg.jpg 5 | /loginscreen-bg-overlay.svg 6 | app-icon.svg 7 | app-icon-*.png 8 | -------------------------------------------------------------------------------- /identifier/src/images/Makefile: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | CONVERT ?= convert 4 | IDENTIFY ?= identify 5 | BASE64 ?= base64 6 | ENVSUBST ?= envsubst 7 | SCOUR ?= scour 8 | INKSCAPE ?= inkscape 9 | 10 | # Variables 11 | 12 | LOGINSCREEN_BG_SRC ?= kopano-augmenting-teamwork-bg-7680.jpg 13 | LOGINSCREEN_BG_OVERLAY_SRC ?= kopano-augmenting-teamwork-overlay.svg 14 | 15 | LOGINSCREEN_BG_WIDTH = $(shell $(IDENTIFY) -format '%w' loginscreen-bg.jpg) 16 | LOGINSCREEN_BG_HEIGHT = $(shell $(IDENTIFY) -format '%h' loginscreen-bg.jpg) 17 | LOGINSCREEN_BG_THUMB_BASE64 = $(shell $(BASE64) -w0 loginscreen-bg-thumb.jpg) 18 | LOGINSCREEN_BG_THUMB_BASE64_SVG = $(shell $(BASE64) -w0 loginscreen-bg-thumb.svg) 19 | 20 | STATIC ?= ../../public/static 21 | ICON ?= kopano-konnect-icon.svg 22 | 23 | # Build 24 | 25 | all: loginscreen-bg.css 26 | 27 | loginscreen-bg.jpg: $(LOGINSCREEN_BG_SRC) 28 | $(CONVERT) -geometry x1080 -strip -interlace Plane -gaussian-blur 0.05 -define jpeg:dct-method=float -quality 75% $< $@ 29 | 30 | loginscreen-bg-thumb.jpg: loginscreen-bg.jpg 31 | $(CONVERT) -geometry x40 -strip -define jpeg:dct-method=float -quality 50% $< $@ 32 | 33 | loginscreen-bg-thumb.svg: loginscreen-bg-thumb.svg.in | loginscreen-bg-thumb.jpg loginscreen-bg.jpg 34 | WIDTH=$(LOGINSCREEN_BG_WIDTH) HEIGHT=$(LOGINSCREEN_BG_HEIGHT) \ 35 | IMAGE_DATA=$(LOGINSCREEN_BG_THUMB_BASE64) \ 36 | $(ENVSUBST) < $< > $@ 37 | 38 | loginscreen-bg-overlay.svg: $(LOGINSCREEN_BG_OVERLAY_SRC) 39 | $(SCOUR) --enable-viewboxing --create-groups --shorten-ids --enable-id-stripping \ 40 | --enable-comment-stripping --disable-embed-rasters --remove-metadata --strip-xml-prolog -p 9 \ 41 | -i $< -o $@ 42 | 43 | loginscreen-bg.css: loginscreen-bg.css.in | loginscreen-bg.jpg loginscreen-bg-thumb.svg loginscreen-bg-overlay.svg 44 | IMAGE_DATA=$(LOGINSCREEN_BG_THUMB_BASE64_SVG) \ 45 | $(ENVSUBST) < $< > $@ 46 | 47 | .PHONY: icons 48 | icons: $(STATIC)/favicon.ico 49 | 50 | $(STATIC)/favicon.ico: app-icon-rounded-256x256.png 51 | $(CONVERT) -background transparent $< -define icon:auto-resize=16,32,48,64,128,256 $@ 52 | 53 | app-icon.svg: $(ICON) 54 | cp -vaf $< $@ 55 | 56 | app-icon-whitebox-256x256.png: app-icon.svg 57 | $(INKSCAPE) -z -e $@.tmp -w 204.8 -h 204.8 -b white -y 1.0 $< 58 | $(CONVERT) $@.tmp -background white -gravity center -extent 256x256 $@ 59 | @$(RM) $@.tmp 60 | 61 | app-icon-rounded-256x256.png: app-icon-whitebox-256x256.png 62 | $(CONVERT) -size 256x256 xc:none -draw "roundrectangle 2,2,252,252,126,126" $@.tmp.png 63 | $(CONVERT) $< -matte $@.tmp.png -compose DstIn -composite $@ 64 | @$(RM) $@.tmp.png 65 | 66 | .PHONY: clean 67 | clean: 68 | $(RM) loginscreen-bg.jpg 69 | $(RM) loginscreen-bg-thumb.jpg 70 | $(RM) loginscreen-bg-thumb.svg 71 | $(RM) loginscreen-bg.css 72 | $(RM) loginscreen-bg-overlay.svg 73 | $(RM) app-icon-*.png || true 74 | $(RM) app-icon.svg || true 75 | -------------------------------------------------------------------------------- /identifier/src/images/kopano-augmenting-teamwork-bg-7680.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kopano-dev/konnect/52c7ab10ee1f2a39323b3f9db1514e0433138e2a/identifier/src/images/kopano-augmenting-teamwork-bg-7680.jpg -------------------------------------------------------------------------------- /identifier/src/images/kopano-konnect-icon.svg: -------------------------------------------------------------------------------- 1 | ../../node_modules/kpop/static/icons/kopano-konnect-icon.svg -------------------------------------------------------------------------------- /identifier/src/images/loginscreen-bg-thumb.svg.in: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /identifier/src/images/loginscreen-bg.css.in: -------------------------------------------------------------------------------- 1 | #bg-thumb { 2 | /* NOTE(longsleep): This is the base64 encoded svg, inlined directly to show 3 | this image as early as possible. 4 | */ 5 | background-image: url("data:image/svg+xml;base64,${IMAGE_DATA}"); 6 | } 7 | -------------------------------------------------------------------------------- /identifier/src/index.js: -------------------------------------------------------------------------------- 1 | import 'kpop/static/css/base.css'; 2 | import 'kpop/static/css/scrollbar.css'; 3 | import 'typeface-roboto'; 4 | import './app.css'; 5 | import './fancy-background.css'; 6 | 7 | import * as kpop from 'kpop/es/version'; 8 | 9 | import * as version from './version'; 10 | 11 | console.info(`Kopano Identifier build version: ${version.build}`); // eslint-disable-line no-console 12 | console.info(`Kopano Kpop build version: ${kpop.build}`); // eslint-disable-line no-console 13 | 14 | // NOTE(longsleep): Load async, this enables code splitting via Webpack. 15 | import(/* webpackChunkName: "identifier-app" */ './app'); 16 | -------------------------------------------------------------------------------- /identifier/src/locales/index.js: -------------------------------------------------------------------------------- 1 | // NOTE(longsleep): This loads all translation files to be included in the 2 | // app bundle. They are not that large. 3 | 4 | // Please keep imports and exports alphabetically sorted. 5 | import de from './de.json'; 6 | import fr from './fr.json'; 7 | import hi from './hi.json'; 8 | import is from './is.json'; 9 | import nb from './nb.json'; 10 | import nl from './nl.json'; 11 | import ptPT from './pt_PT.json'; 12 | import ru from './ru.json'; 13 | 14 | function enableLocales(locales, enabled) { 15 | if (process.env.NODE_ENV !== 'production') { // eslint-disable-line no-undef 16 | return locales; 17 | } 18 | return enabled.reduce(function(value, locale) { 19 | value[locale] = locales[locale]; 20 | return value; 21 | }, {}); 22 | } 23 | 24 | // Locales must follow BCP 47 format (https://tools.ietf.org/html/rfc5646). 25 | const locales = enableLocales({ 26 | de, 27 | 'en-GB': {}, 28 | 'en-US': {}, 29 | fr, 30 | hi, 31 | is, 32 | nb, 33 | nl, 34 | 'pt-PT': ptPT, 35 | ru 36 | }, [ 37 | // List of enabled languages in production builds. 38 | 'de', 39 | 'en-GB', 40 | 'en-US', 41 | 'fr', 42 | 'hi', 43 | 'is', 44 | 'nb', 45 | 'nl', 46 | 'pt-PT', 47 | 'ru' 48 | ]); 49 | 50 | export default locales; 51 | -------------------------------------------------------------------------------- /identifier/src/models/hello.js: -------------------------------------------------------------------------------- 1 | export function newHelloRequest(flow, query) { 2 | const r = {}; 3 | 4 | if (query.prompt) { 5 | // TODO(longsleep): Validate prompt values? 6 | r.prompt = query.prompt; 7 | } 8 | 9 | let selectedFlow = flow; 10 | switch (flow) { 11 | case 'oauth': 12 | case 'consent': 13 | case 'oidc': 14 | r.scope = query.scope || ''; 15 | r.client_id = query.client_id || ''; // eslint-disable-line camelcase 16 | r.redirect_uri = query.redirect_uri || ''; // eslint-disable-line camelcase 17 | if (query.id_token_hint) { 18 | r.id_token_hint = query.id_token_hint; // eslint-disable-line camelcase 19 | } 20 | if (query.max_age) { 21 | r.max_age = query.max_age; // eslint-disable-line camelcase 22 | } 23 | if (query.claims_scope) { 24 | // Add additional scopes from claims request if given. 25 | r.scope += ' ' + query.claims_scope; 26 | } 27 | break; 28 | 29 | default: 30 | selectedFlow = null; 31 | } 32 | 33 | if (selectedFlow) { 34 | r.flow = selectedFlow; 35 | } 36 | 37 | return r; 38 | } 39 | -------------------------------------------------------------------------------- /identifier/src/reducers/common.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_ERROR, 3 | RESET_HELLO, 4 | RECEIVE_HELLO, 5 | SERVICE_WORKER_NEW_CONTENT 6 | } from '../actions/types'; 7 | import queryString from 'query-string'; 8 | 9 | const query = queryString.parse(document.location.search); 10 | const flow = query.flow || ''; 11 | delete query.flow; 12 | 13 | const defaultPathPrefix = (() => { 14 | const root = document.getElementById('root'); 15 | let pathPrefix = root ? root.getAttribute('data-path-prefix') : null; 16 | if (!pathPrefix || pathPrefix === '__PATH_PREFIX__') { 17 | // Not replaced, probably we are running in debug mode or whatever. Use sane default. 18 | pathPrefix = '/signin/v1'; 19 | } 20 | return pathPrefix; 21 | })(); 22 | 23 | const defaultState = { 24 | hello: null, 25 | error: null, 26 | flow: flow, 27 | query: query, 28 | updateAvailable: false, 29 | pathPrefix: defaultPathPrefix 30 | }; 31 | 32 | function commonReducer(state = defaultState, action) { 33 | switch (action.type) { 34 | case RECEIVE_ERROR: 35 | return Object.assign({}, state, { 36 | error: action.error 37 | }); 38 | 39 | case RESET_HELLO: 40 | return Object.assign({}, state, { 41 | hello: null 42 | }); 43 | 44 | case RECEIVE_HELLO: 45 | return Object.assign({}, state, { 46 | hello: { 47 | state: action.state, 48 | username: action.username, 49 | displayName: action.displayName, 50 | details: action.hello 51 | } 52 | }); 53 | 54 | case SERVICE_WORKER_NEW_CONTENT: 55 | return Object.assign({}, state, { 56 | updateAvailable: true 57 | }); 58 | 59 | default: 60 | return state; 61 | } 62 | } 63 | 64 | export default commonReducer; 65 | -------------------------------------------------------------------------------- /identifier/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import commonReducer from './common'; 4 | import loginReducer from './login'; 5 | 6 | const rootReducer = combineReducers({ 7 | common: commonReducer, 8 | login: loginReducer 9 | }); 10 | 11 | export default rootReducer; 12 | -------------------------------------------------------------------------------- /identifier/src/reducers/login.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_VALIDATE_LOGON, 3 | REQUEST_LOGON, 4 | RECEIVE_LOGON, 5 | RECEIVE_LOGOFF, 6 | REQUEST_CONSENT_ALLOW, 7 | REQUEST_CONSENT_CANCEL, 8 | RECEIVE_CONSENT, 9 | UPDATE_INPUT 10 | } from '../actions/types'; 11 | 12 | function loginReducer(state = { 13 | loading: '', 14 | username: '', 15 | password: '', 16 | errors: {} 17 | }, action) { 18 | switch (action.type) { 19 | case RECEIVE_VALIDATE_LOGON: 20 | return Object.assign({}, state, { 21 | errors: action.errors, 22 | loading: '' 23 | }); 24 | 25 | case REQUEST_CONSENT_ALLOW: 26 | case REQUEST_CONSENT_CANCEL: 27 | case REQUEST_LOGON: 28 | return Object.assign({}, state, { 29 | loading: action.type, 30 | errors: {} 31 | }); 32 | 33 | case RECEIVE_CONSENT: 34 | case RECEIVE_LOGON: 35 | if (!action.success) { 36 | return Object.assign({}, state, { 37 | errors: action.errors ? action.errors : {}, 38 | loading: '' 39 | }); 40 | } 41 | return state; 42 | 43 | case RECEIVE_LOGOFF: 44 | return Object.assign({}, state, { 45 | username: '', 46 | password: '' 47 | }); 48 | 49 | case UPDATE_INPUT: 50 | delete state.errors[action.name]; 51 | return Object.assign({}, state, { 52 | [action.name]: action.value 53 | }); 54 | 55 | default: 56 | return state; 57 | } 58 | } 59 | 60 | export default loginReducer; 61 | -------------------------------------------------------------------------------- /identifier/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import { createLogger } from 'redux-logger'; 4 | 5 | import rootReducer from './reducers'; 6 | 7 | const middlewares = [ 8 | thunkMiddleware 9 | ]; 10 | 11 | if (process.env.NODE_ENV === 'development') { // eslint-disable-line no-undef 12 | middlewares.push(createLogger()); // must be last middleware in the chain. 13 | } 14 | 15 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 16 | 17 | const store = createStore( 18 | rootReducer, 19 | composeEnhancers(applyMiddleware( 20 | ...middlewares, 21 | )) 22 | ); 23 | 24 | export default store; 25 | -------------------------------------------------------------------------------- /identifier/src/utils.js: -------------------------------------------------------------------------------- 1 | export function withClientRequestState(obj) { 2 | obj.state = Math.random().toString(36).substring(7); 3 | 4 | return obj; 5 | } 6 | 7 | export function dirname(s) { 8 | return s.replace(/\\/g,'/').replace(/\/[^/]*$/, ''); 9 | } 10 | 11 | export function propertyFromStylesheet(selector, attribute, asURL=false) { 12 | let value; 13 | let sheetHref; 14 | 15 | Array.prototype.some.call(document.styleSheets, function(sheet) { 16 | try { 17 | return Array.prototype.some.call(sheet.cssRules, function(rule) { 18 | sheetHref = sheet.href; 19 | if (selector === rule.selectorText) { 20 | return Array.prototype.some.call(rule.style, function(style) { 21 | if (attribute === style) { 22 | value = rule.style.getPropertyValue(attribute); 23 | return true; 24 | } 25 | 26 | return false; 27 | }); 28 | } 29 | 30 | return false; 31 | }); 32 | } catch(e) { 33 | // Ignore sheels which caused errors. This for example can happen if an 34 | // extension injected styles from an other origin. 35 | return false; 36 | } 37 | }); 38 | 39 | if (value && asURL) { 40 | // This removes url() shit if there. 41 | value = value.match(/(?:\(['|"]?)(.*?)(?:['|"]?\))/)[1]; 42 | if (!value) { 43 | return null; 44 | } 45 | if (sheetHref) { 46 | // URLs in CSS are relative to the CSS - so lets add stuff. 47 | const baseHref = dirname(sheetHref); 48 | value = baseHref + '/' + value; 49 | } 50 | } 51 | 52 | return value; 53 | } 54 | 55 | export function enhanceBodyBackground() { 56 | const bg = propertyFromStylesheet('#bg-enhanced.enhanced', 'background-image', true); 57 | const overlay = propertyFromStylesheet('#bg-enhanced.enhanced::after', 'background-image', true); 58 | 59 | const promises = []; 60 | if (bg) { 61 | promises.push(new Promise(resolve => { 62 | const img = new Image(); 63 | img.onload = () => { 64 | resolve(); 65 | }; 66 | // Set image source to whatever the url from css holds. 67 | img.src = bg; 68 | })); 69 | } 70 | if (overlay) { 71 | promises.push(new Promise(resolve => { 72 | const img = new Image(); 73 | img.onload = () => { 74 | resolve(); 75 | }; 76 | // Set image source to whatever the url from css holds. 77 | img.src = overlay; 78 | })); 79 | } 80 | Promise.all(promises).then(() => { 81 | window.document.getElementById('bg-enhanced').className += ' enhanced'; 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /identifier/src/version.js: -------------------------------------------------------------------------------- 1 | /*global process: true*/ 2 | 3 | const build = process.env.REACT_APP_KOPANO_BUILD || '0.0.0-no-proper-build'; 4 | 5 | export { 6 | build 7 | }; 8 | -------------------------------------------------------------------------------- /identifier/trampolin.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identifier 19 | 20 | import ( 21 | "html/template" 22 | "net/http" 23 | "net/url" 24 | 25 | "stash.kopano.io/kc/konnect/version" 26 | ) 27 | 28 | var trampolinTemplate = template.Must(template.New("trampolin").Parse(` 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | `)) 39 | 40 | var trampolinScript = []byte(`(function(window) { 41 | window.location.replace(document.body.getAttribute('trampolin')); 42 | }(window)); 43 | `) 44 | 45 | var trampolinVersion = url.QueryEscape(version.Version) 46 | 47 | type trampolinData struct { 48 | URI string 49 | Version string 50 | } 51 | 52 | func (i *Identifier) writeTrampolinHTML(rw http.ResponseWriter, req *http.Request, uri *url.URL) { 53 | data := &trampolinData{ 54 | URI: uri.String(), 55 | Version: trampolinVersion, 56 | } 57 | 58 | rw.Header().Set("Content-Type", "text/html") 59 | rw.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 60 | rw.Header().Set("Pragma", "no-cache") 61 | rw.Header().Set("Expires", "0") 62 | err := trampolinTemplate.Execute(rw, data) 63 | if err != nil { 64 | i.logger.WithError(err).Errorln("failed to write trampolin") 65 | } 66 | } 67 | 68 | func (i *Identifier) writeTrampolinScript(rw http.ResponseWriter, req *http.Request) { 69 | rw.Header().Set("Content-Type", "application/javascript") 70 | rw.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 71 | 72 | rw.Write(trampolinScript) 73 | } 74 | -------------------------------------------------------------------------------- /identifier/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identifier 19 | 20 | import ( 21 | "net/http" 22 | "time" 23 | ) 24 | 25 | var ( 26 | farPastExpiryTime = time.Unix(0, 0) 27 | farPastExpiryTimeHTTPHeaderString = farPastExpiryTime.UTC().Format(http.TimeFormat) 28 | ) 29 | 30 | func addCommonResponseHeaders(header http.Header) { 31 | header.Set("X-Frame-Options", "DENY") 32 | header.Set("X-XSS-Protection", "1; mode=block") 33 | header.Set("X-Content-Type-Options", "nosniff") 34 | header.Set("Referrer-Policy", "origin") 35 | } 36 | 37 | func addNoCacheResponseHeaders(header http.Header) { 38 | header.Set("Cache-Control", "no-cache, no-store, must-revalidate") 39 | header.Set("Pragma", "no-cache") 40 | header.Set("Expires", farPastExpiryTimeHTTPHeaderString) 41 | } 42 | -------------------------------------------------------------------------------- /identity/auth.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identity 19 | 20 | import ( 21 | "time" 22 | 23 | "github.com/dgrijalva/jwt-go" 24 | 25 | "stash.kopano.io/kc/konnect/oidc/payload" 26 | ) 27 | 28 | // AuthRecord is an interface which provides identity auth information with scopes and claims.. 29 | type AuthRecord interface { 30 | Manager() Manager 31 | Subject() string 32 | AuthorizedScopes() map[string]bool 33 | AuthorizeScopes(map[string]bool) 34 | AuthorizedClaims() *payload.ClaimsRequest 35 | AuthorizeClaims(*payload.ClaimsRequest) 36 | Claims(...string) []jwt.Claims 37 | 38 | User() PublicUser 39 | SetUser(PublicUser) 40 | 41 | LoggedOn() (bool, time.Time) 42 | SetAuthTime(time.Time) 43 | } 44 | -------------------------------------------------------------------------------- /identity/authorities/models.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package authorities 19 | 20 | import ( 21 | "context" 22 | "net/http" 23 | "net/url" 24 | 25 | "gopkg.in/square/go-jose.v2" 26 | ) 27 | 28 | // Supported Authority kind string values. 29 | const ( 30 | AuthorityTypeOIDC = "oidc" 31 | AuthorityTypeSAML2 = "saml2" 32 | ) 33 | 34 | type authorityRegistrationData struct { 35 | ID string `yaml:"id"` 36 | Name string `yaml:"name"` 37 | AuthorityType string `yaml:"authority_type"` 38 | 39 | Iss string `yaml:"iss"` 40 | 41 | ClientID string `yaml:"client_id"` 42 | ClientSecret string `yaml:"client_secret"` 43 | 44 | EntityID string `yaml:"entity_id"` 45 | 46 | Trusted bool `yaml:"trusted"` 47 | Insecure bool `yaml:"insecure"` 48 | Default bool `yaml:"default"` 49 | Discover *bool `yaml:"discover"` 50 | 51 | Scopes []string `yaml:"scopes"` 52 | ResponseType string `yaml:"response_type"` 53 | CodeChallengeMethod string `yaml:"code_challenge_method"` 54 | 55 | RawMetadataEndpoint string `yaml:"metadata_endpoint"` 56 | RawAuthorizationEndpoint string `yaml:"authorization_endpoint"` 57 | 58 | JWKS *jose.JSONWebKeySet `yaml:"jwks"` 59 | 60 | IdentityClaimName string `yaml:"identity_claim_name"` 61 | 62 | IdentityAliases map[string]string `yaml:"identity_aliases,flow"` 63 | IdentityAliasRequired bool `yaml:"identity_alias_required"` 64 | 65 | EndSessionEnabled bool `yaml:"end_session_enabled"` 66 | } 67 | 68 | type authorityRegistryData struct { 69 | Authorities []*authorityRegistrationData `yaml:"authorities,flow"` 70 | } 71 | 72 | // AuthorityRegistration defines an authority with its properties. 73 | type AuthorityRegistration interface { 74 | ID() string 75 | Name() string 76 | AuthorityType() string 77 | 78 | Authority() *Details 79 | Issuer() string 80 | 81 | Validate() error 82 | 83 | Initialize(ctx context.Context, registry *Registry) error 84 | 85 | MakeRedirectAuthenticationRequestURL(state string) (*url.URL, map[string]interface{}, error) 86 | MakeRedirectEndSessionRequestURL(ref interface{}, state string) (*url.URL, map[string]interface{}, error) 87 | MakeRedirectEndSessionResponseURL(req interface{}, state string) (*url.URL, map[string]interface{}, error) 88 | 89 | ParseStateResponse(req *http.Request, state string, extra map[string]interface{}) (interface{}, error) 90 | 91 | ValidateIdpEndSessionRequest(req interface{}, state string) (bool, error) 92 | ValidateIdpEndSessionResponse(res interface{}, state string) (bool, error) 93 | 94 | IdentityClaimValue(data interface{}) (string, map[string]interface{}, error) 95 | 96 | Metadata() AuthorityMetadata 97 | } 98 | 99 | type AuthorityMetadata interface { 100 | } 101 | -------------------------------------------------------------------------------- /identity/authorities/samlext/logoutrequest.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package samlext 19 | 20 | import ( 21 | "bytes" 22 | "compress/flate" 23 | "encoding/base64" 24 | "net/url" 25 | 26 | "github.com/beevik/etree" 27 | "github.com/crewjam/saml" 28 | ) 29 | 30 | type LogoutRequest struct { 31 | *saml.LogoutRequest 32 | } 33 | 34 | // Redirect returns a URL suitable for using the redirect binding with the response. 35 | func (req *LogoutRequest) Redirect(relayState string) *url.URL { 36 | w := &bytes.Buffer{} 37 | w1 := base64.NewEncoder(base64.StdEncoding, w) 38 | w2, _ := flate.NewWriter(w1, 9) 39 | doc := etree.NewDocument() 40 | doc.SetRoot(req.Element()) 41 | if _, err := doc.WriteTo(w2); err != nil { 42 | panic(err) 43 | } 44 | w2.Close() 45 | w1.Close() 46 | 47 | rv, _ := url.Parse(req.Destination) 48 | 49 | query := rv.Query() 50 | query.Set("SAMLRequest", w.String()) 51 | if relayState != "" { 52 | query.Set("RelayState", relayState) 53 | } 54 | rv.RawQuery = query.Encode() 55 | 56 | return rv 57 | } 58 | -------------------------------------------------------------------------------- /identity/authorities/samlext/logoutresponse.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package samlext 19 | 20 | import ( 21 | "bytes" 22 | "compress/flate" 23 | "encoding/base64" 24 | "encoding/xml" 25 | "fmt" 26 | "net/url" 27 | 28 | "github.com/crewjam/saml" 29 | "stash.kopano.io/kgol/rndm" 30 | ) 31 | 32 | func MakeLogoutResponse(sp *saml.ServiceProvider, req *saml.LogoutRequest, status *saml.Status, binding string) (*LogoutResponse, error) { 33 | 34 | res := &LogoutResponse{&saml.LogoutResponse{ 35 | ID: fmt.Sprintf("id-%x", rndm.GenerateRandomBytes(20)), 36 | InResponseTo: req.ID, 37 | 38 | Version: "2.0", 39 | IssueInstant: saml.TimeNow(), 40 | Destination: sp.GetSLOBindingLocation(binding), 41 | 42 | Issuer: &saml.Issuer{ 43 | Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity", 44 | Value: firstSet(sp.EntityID, sp.MetadataURL.String()), 45 | }, 46 | }} 47 | 48 | if status != nil { 49 | res.LogoutResponse.Status = *status 50 | } 51 | 52 | return res, nil 53 | } 54 | 55 | func firstSet(a, b string) string { 56 | if a == "" { 57 | return b 58 | } 59 | return a 60 | } 61 | 62 | type LogoutResponse struct { 63 | *saml.LogoutResponse 64 | } 65 | 66 | // Redirect returns a URL suitable for using the redirect binding with the response. 67 | func (res *LogoutResponse) Redirect(relayState string) *url.URL { 68 | w := &bytes.Buffer{} 69 | w1 := base64.NewEncoder(base64.StdEncoding, w) 70 | w2, _ := flate.NewWriter(w1, 9) 71 | e := xml.NewEncoder(w2) 72 | if err := e.Encode(res); err != nil { 73 | panic(err) 74 | } 75 | w2.Close() 76 | w1.Close() 77 | 78 | rv, _ := url.Parse(res.Destination) 79 | 80 | query := rv.Query() 81 | query.Set("SAMLResponse", w.String()) 82 | if relayState != "" { 83 | query.Set("RelayState", relayState) 84 | } 85 | rv.RawQuery = query.Encode() 86 | 87 | return rv 88 | } 89 | -------------------------------------------------------------------------------- /identity/clients/claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package clients 19 | 20 | import ( 21 | "github.com/dgrijalva/jwt-go" 22 | ) 23 | 24 | // RegistrationClaims are claims used to with dynamic clients. 25 | type RegistrationClaims struct { 26 | jwt.StandardClaims 27 | 28 | *ClientRegistration 29 | } 30 | 31 | // Valid implements the jwt claims interface. 32 | func (crc RegistrationClaims) Valid() error { 33 | return crc.StandardClaims.Valid() 34 | } 35 | -------------------------------------------------------------------------------- /identity/clients/clients.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package clients 19 | 20 | import ( 21 | "crypto" 22 | "net/url" 23 | ) 24 | 25 | // Details hold detail information about clients identified by ID. 26 | type Details struct { 27 | ID string `json:"id"` 28 | DisplayName string `json:"display_name"` 29 | RedirectURI string `json:"redirect_uri"` 30 | Trusted bool `json:"trusted"` 31 | 32 | Registration *ClientRegistration `json:"-"` 33 | } 34 | 35 | // A Secured is a client records public key identified by ID. 36 | type Secured struct { 37 | ID string 38 | DisplayName string 39 | ApplicationType string 40 | 41 | Kid string 42 | PublicKey crypto.PublicKey 43 | 44 | TrustedScopes []string 45 | 46 | Registration *ClientRegistration 47 | } 48 | 49 | // IsLocalNativeHTTPURI returns true if the provided URI qualifies to be used 50 | // as http redirect URI for a native client. 51 | func IsLocalNativeHTTPURI(uri *url.URL) bool { 52 | if uri.Scheme != "http" { 53 | return false 54 | } 55 | return IsLocalNativeHostURI(uri) 56 | } 57 | 58 | // IsLocalNativeHostURI returns true if the provided URI hostname is considered 59 | // as localhost for a native client. 60 | func IsLocalNativeHostURI(uri *url.URL) bool { 61 | hostname := uri.Hostname() 62 | return hostname == "localhost" || hostname == "127.0.0.1" || hostname == "::1" 63 | } 64 | -------------------------------------------------------------------------------- /identity/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identity 19 | 20 | import ( 21 | "net/url" 22 | 23 | "github.com/sirupsen/logrus" 24 | ) 25 | 26 | // Config defines a IdentityManager's configuration settings. 27 | type Config struct { 28 | SignInFormURI *url.URL 29 | SignedOutURI *url.URL 30 | 31 | ScopesSupported []string 32 | 33 | Logger logrus.FieldLogger 34 | } 35 | -------------------------------------------------------------------------------- /identity/context.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identity 19 | 20 | import ( 21 | "context" 22 | ) 23 | 24 | // key is an unexported type for keys defined in this package. 25 | // This prevents collisions with keys defined in other packages. 26 | type key int 27 | 28 | // authRecordKey is the key for identity.AuthRecord in Contexts. It is 29 | // unexported; clients use identity.NewContext and identity.FromContext 30 | // instead of using this key directly. 31 | var authRecordKey key 32 | 33 | // NewContext returns a new Context that carries value auth. 34 | func NewContext(ctx context.Context, auth AuthRecord) context.Context { 35 | return context.WithValue(ctx, authRecordKey, auth) 36 | } 37 | 38 | // FromContext returns the AuthRecord value stored in ctx, if any. 39 | func FromContext(ctx context.Context) (AuthRecord, bool) { 40 | auth, ok := ctx.Value(authRecordKey).(AuthRecord) 41 | return auth, ok 42 | } 43 | -------------------------------------------------------------------------------- /identity/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identity 19 | 20 | import ( 21 | "net/url" 22 | ) 23 | 24 | // IsHandledError is an error which tells that the backend has handled 25 | // the request and all further handling should stop 26 | type IsHandledError struct { 27 | } 28 | 29 | // Error implements the error interface. 30 | func (err *IsHandledError) Error() string { 31 | return "is_handled" 32 | } 33 | 34 | // RedirectError is an error which backends can return if a 35 | // redirection is required. 36 | type RedirectError struct { 37 | id string 38 | redirectURI *url.URL 39 | } 40 | 41 | // NewRedirectError creates a new corresponding error with the 42 | // provided id and redirect URL. 43 | func NewRedirectError(id string, redirectURI *url.URL) *RedirectError { 44 | return &RedirectError{ 45 | id: id, 46 | redirectURI: redirectURI, 47 | } 48 | } 49 | 50 | // Error implements the error interface. 51 | func (err *RedirectError) Error() string { 52 | return err.id 53 | } 54 | 55 | // RedirectURI returns the redirection URL of the accociated error. 56 | func (err *RedirectError) RedirectURI() *url.URL { 57 | return err.redirectURI 58 | } 59 | 60 | // LoginRequiredError which backends can return to indicate that sign-in is 61 | // required. 62 | type LoginRequiredError struct { 63 | id string 64 | signInURI *url.URL 65 | } 66 | 67 | // NewLoginRequiredError creates a new corresponding error with the provided id. 68 | func NewLoginRequiredError(id string, signInURI *url.URL) *LoginRequiredError { 69 | return &LoginRequiredError{ 70 | id: id, 71 | signInURI: signInURI, 72 | } 73 | } 74 | 75 | // Error implements the error interface. 76 | func (err *LoginRequiredError) Error() string { 77 | return err.id 78 | } 79 | 80 | // SignInURI returns the sign-in URL of the accociated error. 81 | func (err *LoginRequiredError) SignInURI() *url.URL { 82 | return err.signInURI 83 | } 84 | -------------------------------------------------------------------------------- /identity/manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identity 19 | 20 | import ( 21 | "context" 22 | "net/http" 23 | 24 | "github.com/gorilla/mux" 25 | 26 | "stash.kopano.io/kc/konnect/oidc/payload" 27 | ) 28 | 29 | // Manager is a interface to define a identity manager. 30 | type Manager interface { 31 | Authenticate(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, next Manager) (AuthRecord, error) 32 | Authorize(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, auth AuthRecord) (AuthRecord, error) 33 | EndSession(ctx context.Context, rw http.ResponseWriter, req *http.Request, esr *payload.EndSessionRequest) error 34 | 35 | ApproveScopes(ctx context.Context, sub string, audience string, approvedScopesList map[string]bool) (string, error) 36 | ApprovedScopes(ctx context.Context, sub string, audience string, ref string) (map[string]bool, error) 37 | 38 | Fetch(ctx context.Context, userID string, sessionRef *string, scopes map[string]bool, requestedClaimsMaps []*payload.ClaimsRequestMap) (AuthRecord, bool, error) 39 | 40 | Name() string 41 | ScopesSupported(scopes map[string]bool) []string 42 | ClaimsSupported(claims []string) []string 43 | 44 | AddRoutes(ctx context.Context, router *mux.Router) 45 | 46 | OnSetLogon(func(ctx context.Context, rw http.ResponseWriter, user User) error) error 47 | OnUnsetLogon(func(ctx context.Context, rw http.ResponseWriter) error) error 48 | } 49 | -------------------------------------------------------------------------------- /identity/managers/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package managers 19 | 20 | import ( 21 | "encoding/base64" 22 | 23 | "golang.org/x/crypto/blake2b" 24 | 25 | konnectoidc "stash.kopano.io/kc/konnect/oidc" 26 | ) 27 | 28 | func setupSupportedScopes(scopes []string, extra []string, override []string) []string { 29 | if len(override) > 0 { 30 | return override 31 | } 32 | 33 | return append(scopes, extra...) 34 | } 35 | 36 | func getPublicSubject(sub []byte, extra []byte) (string, error) { 37 | // Hash the raw subject with a konnect specific salt. 38 | hasher, err := blake2b.New512([]byte(konnectoidc.KonnectIDTokenSubjectSaltV1)) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | hasher.Write(sub) 44 | hasher.Write([]byte(" ")) 45 | hasher.Write(extra) 46 | 47 | // NOTE(longsleep): URL safe encoding for subject is important since many 48 | // third party applications validate this with rather strict patterns. 49 | s := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil)) 50 | return s + "@konnect", nil 51 | } 52 | -------------------------------------------------------------------------------- /identity/user.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package identity 19 | 20 | import ( 21 | "github.com/dgrijalva/jwt-go" 22 | ) 23 | 24 | // User defines a most simple user with an id defined as subject. 25 | type User interface { 26 | Subject() string 27 | } 28 | 29 | // UserWithEmail is a User with Email. 30 | type UserWithEmail interface { 31 | User 32 | Email() string 33 | EmailVerified() bool 34 | } 35 | 36 | // UserWithProfile is a User with Name. 37 | type UserWithProfile interface { 38 | User 39 | Name() string 40 | FamilyName() string 41 | GivenName() string 42 | } 43 | 44 | // UserWithID is a User with a locally unique numeric id. 45 | type UserWithID interface { 46 | User 47 | ID() int64 48 | } 49 | 50 | // UserWithUniqueID is a User with a unique string id. 51 | type UserWithUniqueID interface { 52 | User 53 | UniqueID() string 54 | } 55 | 56 | // UserWithUsername is a User with an username different from subject. 57 | type UserWithUsername interface { 58 | User 59 | Username() string 60 | } 61 | 62 | // UserWithClaims is a User with jwt claims. 63 | type UserWithClaims interface { 64 | User 65 | Claims() jwt.MapClaims 66 | } 67 | 68 | // UserWithScopedClaims is a user with jwt claims bound to provided scopes. 69 | type UserWithScopedClaims interface { 70 | User 71 | ScopedClaims(authorizedScopes map[string]bool) jwt.MapClaims 72 | } 73 | 74 | // UserWithSessionRef is a user which supports an underlaying session reference. 75 | type UserWithSessionRef interface { 76 | User 77 | SessionRef() *string 78 | } 79 | 80 | // PublicUser is a user with a public Subject and a raw id. 81 | type PublicUser interface { 82 | Subject() string 83 | Raw() string 84 | } 85 | -------------------------------------------------------------------------------- /managers/managers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package managers 19 | 20 | import ( 21 | "fmt" 22 | ) 23 | 24 | // ServiceUsesManagers is an interface for service which register to managers. 25 | type ServiceUsesManagers interface { 26 | RegisterManagers(mgrs *Managers) error 27 | } 28 | 29 | // Managers is a registry for named managers. 30 | type Managers struct { 31 | registry map[string]interface{} 32 | } 33 | 34 | // New creates a new Managers. 35 | func New() *Managers { 36 | return &Managers{ 37 | registry: make(map[string]interface{}), 38 | } 39 | } 40 | 41 | // Set adds the provided manager with the provided name to the accociated 42 | // Managers. 43 | func (m *Managers) Set(name string, manager interface{}) { 44 | m.registry[name] = manager 45 | } 46 | 47 | // Get returns the manager identified by the given name from the accociated 48 | // managers. 49 | func (m *Managers) Get(name string) (interface{}, bool) { 50 | manager, ok := m.registry[name] 51 | 52 | return manager, ok 53 | } 54 | 55 | // Must returns the manager indentified by the given name or panics. 56 | func (m *Managers) Must(name string) interface{} { 57 | manager, ok := m.Get(name) 58 | if !ok { 59 | panic(fmt.Errorf("manager %s not found", name)) 60 | } 61 | 62 | return manager 63 | } 64 | 65 | // Apply registers the accociated manager's registered managers. 66 | func (m *Managers) Apply() error { 67 | for _, manager := range m.registry { 68 | if service, ok := manager.(ServiceUsesManagers); ok { 69 | err := service.RegisterManagers(m) 70 | if err != nil { 71 | return err 72 | } 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /oidc/claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package oidc 19 | 20 | import ( 21 | "github.com/dgrijalva/jwt-go" 22 | ) 23 | 24 | // IDTokenClaims define the claims found in OIDC ID Tokens. 25 | type IDTokenClaims struct { 26 | jwt.StandardClaims 27 | 28 | Nonce string `json:"nonce,omitempty"` 29 | AuthTime int64 `json:"auth_time,omitempty"` 30 | AccessTokenHash string `json:"at_hash,omitempty"` 31 | CodeHash string `json:"c_hash,omitempty"` 32 | 33 | *ProfileClaims 34 | *EmailClaims 35 | 36 | *SessionClaims 37 | } 38 | 39 | // Valid implements the jwt.Claims interface. 40 | func (c IDTokenClaims) Valid() (err error) { 41 | return c.StandardClaims.Valid() 42 | } 43 | 44 | // ProfileClaims define the claims for the OIDC profile scope. 45 | // https://openid.net/specs/openid-connect-basic-1_0.html#Scopes 46 | type ProfileClaims struct { 47 | Name string `json:"name,omitempty"` 48 | FamilyName string `json:"family_name,omitempty"` 49 | GivenName string `json:"given_name,omitempty"` 50 | PreferredUsername string `json:"preferred_username,omitempty"` 51 | } 52 | 53 | // NewProfileClaims return a new ProfileClaims set from the provided 54 | // jwt.Claims or nil. 55 | func NewProfileClaims(claims jwt.Claims) *ProfileClaims { 56 | if claims == nil { 57 | return nil 58 | } 59 | 60 | return claims.(*ProfileClaims) 61 | } 62 | 63 | // Valid implements the jwt.Claims interface. 64 | func (c ProfileClaims) Valid() error { 65 | return nil 66 | } 67 | 68 | // EmailClaims define the claims for the OIDC email scope. 69 | // https://openid.net/specs/openid-connect-basic-1_0.html#Scopes 70 | type EmailClaims struct { 71 | Email string `json:"email,omitempty"` 72 | EmailVerified bool `json:"email_verified"` 73 | } 74 | 75 | // NewEmailClaims return a new EmailClaims set from the provided 76 | // jwt.Claims or nil. 77 | func NewEmailClaims(claims jwt.Claims) *EmailClaims { 78 | if claims == nil { 79 | return nil 80 | } 81 | 82 | return claims.(*EmailClaims) 83 | } 84 | 85 | // Valid implements the jwt.Claims interface. 86 | func (c EmailClaims) Valid() error { 87 | return nil 88 | } 89 | 90 | // UserInfoClaims define the claims defined by the OIDC UserInfo 91 | // endpoint. 92 | type UserInfoClaims struct { 93 | Subject string `json:"sub,omitempty"` 94 | } 95 | 96 | // Valid implements the jwt.Claims interface. 97 | func (c UserInfoClaims) Valid() error { 98 | return nil 99 | } 100 | 101 | // SessionClaims define claims related to front end sessions, for example as 102 | // specified by https://openid.net/specs/openid-connect-frontchannel-1_0.html 103 | type SessionClaims struct { 104 | SessionID string `json:"sid,omitempty"` 105 | } 106 | -------------------------------------------------------------------------------- /oidc/code/manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package code 19 | 20 | import ( 21 | "stash.kopano.io/kc/konnect/identity" 22 | "stash.kopano.io/kc/konnect/oidc/payload" 23 | ) 24 | 25 | // Record bundles the data storedi in a code manager. 26 | type Record struct { 27 | AuthenticationRequest *payload.AuthenticationRequest 28 | Auth identity.AuthRecord 29 | Session *payload.Session 30 | } 31 | 32 | // Manager is a interface defining a code manager. 33 | type Manager interface { 34 | Create(record *Record) (string, error) 35 | Pop(code string) (*Record, bool) 36 | } 37 | -------------------------------------------------------------------------------- /oidc/code/managers/memorymap.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package managers 19 | 20 | import ( 21 | "context" 22 | "time" 23 | 24 | "github.com/orcaman/concurrent-map" 25 | "stash.kopano.io/kgol/rndm" 26 | 27 | "stash.kopano.io/kc/konnect/oidc/code" 28 | ) 29 | 30 | const ( 31 | codeValidDuration = 2 * time.Minute 32 | ) 33 | 34 | // Manager provides the api and state for OIDC code generation and token 35 | // exchange. The CodeManager's methods are safe to call from multiple Go 36 | // routines. 37 | type memoryMapManager struct { 38 | table cmap.ConcurrentMap 39 | codeDuration time.Duration 40 | } 41 | 42 | type codeRequestRecord struct { 43 | record *code.Record 44 | //ar *payload.AuthenticationRequest 45 | //auth identity.AuthRecord 46 | when time.Time 47 | } 48 | 49 | // NewMemoryMapManager creates a new CodeManager. 50 | func NewMemoryMapManager(ctx context.Context) code.Manager { 51 | cm := &memoryMapManager{ 52 | table: cmap.New(), 53 | } 54 | 55 | // Cleanup function. 56 | go func() { 57 | ticker := time.NewTicker(30 * time.Second) 58 | defer ticker.Stop() 59 | for { 60 | select { 61 | case <-ticker.C: 62 | cm.purgeExpired() 63 | case <-ctx.Done(): 64 | return 65 | } 66 | 67 | } 68 | }() 69 | 70 | return cm 71 | } 72 | 73 | func (cm *memoryMapManager) purgeExpired() { 74 | var expired []string 75 | deadline := time.Now().Add(-codeValidDuration) 76 | var record *codeRequestRecord 77 | for entry := range cm.table.IterBuffered() { 78 | record = entry.Val.(*codeRequestRecord) 79 | if record.when.Before(deadline) { 80 | expired = append(expired, entry.Key) 81 | } 82 | } 83 | for _, code := range expired { 84 | cm.table.Remove(code) 85 | } 86 | } 87 | 88 | // Create creates a new random code string, stores it together with the provided 89 | // values in the accociated CodeManager's table and returns the code. 90 | func (cm *memoryMapManager) Create(record *code.Record) (string, error) { 91 | code := rndm.GenerateRandomString(24) 92 | 93 | rr := &codeRequestRecord{ 94 | record: record, 95 | when: time.Now(), 96 | } 97 | cm.table.Set(code, rr) 98 | 99 | return code, nil 100 | } 101 | 102 | // Pop looks up the provided code in the accociated CodeManagers's table. If 103 | // found it returns the authentication request and backend record plus true. 104 | // When not found, both values return as nil plus false. 105 | func (cm *memoryMapManager) Pop(code string) (*code.Record, bool) { 106 | stored, found := cm.table.Pop(code) 107 | if !found { 108 | return nil, false 109 | } 110 | rr := stored.(*codeRequestRecord) 111 | 112 | return rr.record, true 113 | } 114 | -------------------------------------------------------------------------------- /oidc/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package oidc 19 | 20 | import ( 21 | "fmt" 22 | "net/http" 23 | 24 | "stash.kopano.io/kc/konnect/utils" 25 | ) 26 | 27 | // OAuth2Error defines a general OAuth2 error with id and decription. 28 | type OAuth2Error struct { 29 | ErrorID string `json:"error"` 30 | ErrorDescription string `json:"error_description"` 31 | } 32 | 33 | // Error implements the error interface. 34 | func (err *OAuth2Error) Error() string { 35 | return err.ErrorID 36 | } 37 | 38 | // Description implements the ErrorWithDescription interface. 39 | func (err *OAuth2Error) Description() string { 40 | return err.ErrorDescription 41 | } 42 | 43 | // NewOAuth2Error creates a new error with id and description. 44 | func NewOAuth2Error(id string, description string) utils.ErrorWithDescription { 45 | return &OAuth2Error{id, description} 46 | } 47 | 48 | // WriteWWWAuthenticateError writes the provided error with the provided 49 | // http status code to the provided http response writer as a 50 | // WWW-Authenticate header with comma separated fields for id and 51 | // description. 52 | func WriteWWWAuthenticateError(rw http.ResponseWriter, code int, err error) { 53 | if code == 0 { 54 | code = http.StatusUnauthorized 55 | } 56 | 57 | var description string 58 | switch err.(type) { 59 | case utils.ErrorWithDescription: 60 | description = err.(utils.ErrorWithDescription).Description() 61 | default: 62 | } 63 | 64 | rw.Header().Set("WWW-Authenticate", fmt.Sprintf("error=\"%s\", error_description=\"%s\"", err.Error(), description)) 65 | rw.WriteHeader(code) 66 | } 67 | 68 | // IsErrorWithID returns true if the given error is an OAuth2Error error with 69 | // the given ID. 70 | func IsErrorWithID(err error, id string) bool { 71 | if err == nil { 72 | return false 73 | } 74 | 75 | oauth2Error, ok := err.(*OAuth2Error) 76 | if !ok { 77 | return false 78 | } 79 | 80 | return oauth2Error.ErrorID == id 81 | } 82 | -------------------------------------------------------------------------------- /oidc/oidc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package oidc 19 | 20 | // KonnectIDTokenSubjectSaltV1 is the salt value used when hasing Subjects in 21 | // ID tokens created by Konnect. 22 | const KonnectIDTokenSubjectSaltV1 = "konnect-IDToken-v1" 23 | -------------------------------------------------------------------------------- /oidc/payload/request.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package payload 19 | 20 | import ( 21 | "errors" 22 | 23 | "github.com/dgrijalva/jwt-go" 24 | 25 | "stash.kopano.io/kc/konnect/identity/clients" 26 | ) 27 | 28 | // RequestObjectClaims holds the incoming request object claims provided as 29 | // JWT via request parameter to OpenID Connect 1.0 authorization endpoint 30 | // requests specified at 31 | // https://openid.net/specs/openid-connect-core-1_0.html#JWTRequests 32 | type RequestObjectClaims struct { 33 | jwt.StandardClaims 34 | 35 | RawScope string `json:"scope"` 36 | Claims *ClaimsRequest `json:"claims"` 37 | RawResponseType string `json:"response_type"` 38 | ResponseMode string `json:"response_mode"` 39 | ClientID string `json:"client_id"` 40 | RawRedirectURI string `json:"redirect_uri"` 41 | State string `json:"state"` 42 | Nonce string `json:"nonce"` 43 | RawPrompt string `json:"prompt"` 44 | RawIDTokenHint string `json:"id_token_hint"` 45 | RawMaxAge string `json:"max_age"` 46 | 47 | RawRegistration string `json:"registration"` 48 | 49 | CodeChallenge string `json:"code_challenge"` 50 | CodeChallengeMethod string `json:"code_challenge_method"` 51 | 52 | client *clients.Secured 53 | } 54 | 55 | // SetSecure sets the provided client as owner of the accociated claims. 56 | func (roc *RequestObjectClaims) SetSecure(client *clients.Secured) error { 57 | if roc.ClientID != client.ID { 58 | return errors.New("client ID mismatch") 59 | } 60 | 61 | roc.client = client 62 | 63 | return nil 64 | } 65 | 66 | // Secure returns the accociated secure client or nil if not secure. 67 | func (roc *RequestObjectClaims) Secure() *clients.Secured { 68 | return roc.client 69 | } 70 | -------------------------------------------------------------------------------- /oidc/payload/schema.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package payload 19 | 20 | import ( 21 | "encoding/json" 22 | "reflect" 23 | 24 | "github.com/gorilla/schema" 25 | ) 26 | 27 | var decoder = schema.NewDecoder() 28 | var encoder = schema.NewEncoder() 29 | 30 | // DecodeSchema decodes request form data into the provided dst schema struct. 31 | func DecodeSchema(dst interface{}, src map[string][]string) error { 32 | return decoder.Decode(dst, src) 33 | } 34 | 35 | // EncodeSchema encodes the provided src schema to the provided map. 36 | func EncodeSchema(src interface{}, dst map[string][]string) error { 37 | return encoder.Encode(src, dst) 38 | } 39 | 40 | // ConvertOIDCClaimsRequest is a converter function for oidc.ClaimsRequest data 41 | // provided in URL schema. 42 | func ConvertOIDCClaimsRequest(value string) reflect.Value { 43 | v := ClaimsRequest{} 44 | 45 | if err := json.Unmarshal([]byte(value), &v); err != nil { 46 | return reflect.Value{} 47 | } 48 | 49 | return reflect.ValueOf(v) 50 | } 51 | 52 | func init() { 53 | decoder.IgnoreUnknownKeys(true) 54 | decoder.RegisterConverter(ClaimsRequest{}, ConvertOIDCClaimsRequest) 55 | } 56 | -------------------------------------------------------------------------------- /oidc/payload/session.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package payload 19 | 20 | // Session defines a Provider's session with a String identifier for a Session. 21 | // This represents a Session of a User Agent or device for a logged-in End-User 22 | // at an RP. Different ID values are used to identify distinct sessions. This 23 | // is implemented as defined in the OIDC Front Channel logout extension 24 | // https://openid.net/specs/openid-connect-frontchannel-1_0.html#OPLogout 25 | type Session struct { 26 | Version int 27 | ID string 28 | Sub string 29 | Provider string 30 | } 31 | -------------------------------------------------------------------------------- /oidc/payload/userinfo.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package payload 19 | 20 | import ( 21 | "stash.kopano.io/kc/konnect/oidc" 22 | ) 23 | 24 | // UserInfoResponse defines the data returned from the OIDC UserInfo 25 | // endpoint. 26 | type UserInfoResponse struct { 27 | oidc.UserInfoClaims 28 | *oidc.ProfileClaims 29 | *oidc.EmailClaims 30 | } 31 | -------------------------------------------------------------------------------- /oidc/payload/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package payload 19 | 20 | import ( 21 | "encoding/json" 22 | ) 23 | 24 | // ToMap is a helper function to convert the provided payload struct to 25 | // a map type which can be used to extend the payload data with additional fields. 26 | func ToMap(payload interface{}) (map[string]interface{}, error) { 27 | // NOTE(longsleep): This implementation sucks, marshal to JSON and unmarshal 28 | // again - rly? 29 | intermediate, err := json.Marshal(payload) 30 | if err != nil { 31 | return nil, err 32 | } 33 | claims := make(map[string]interface{}) 34 | err = json.Unmarshal(intermediate, &claims) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return claims, nil 40 | } 41 | -------------------------------------------------------------------------------- /oidc/provider/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package provider 19 | 20 | import ( 21 | "time" 22 | 23 | "stash.kopano.io/kc/konnect/config" 24 | ) 25 | 26 | // Config defines a Provider's configuration settings. 27 | type Config struct { 28 | Config *config.Config 29 | 30 | IssuerIdentifier string 31 | WellKnownPath string 32 | JwksPath string 33 | AuthorizationPath string 34 | TokenPath string 35 | UserInfoPath string 36 | EndSessionPath string 37 | CheckSessionIframePath string 38 | RegistrationPath string 39 | 40 | BrowserStateCookiePath string 41 | BrowserStateCookieName string 42 | 43 | SessionCookiePath string 44 | SessionCookieName string 45 | 46 | AccessTokenDuration time.Duration 47 | IDTokenDuration time.Duration 48 | RefreshTokenDuration time.Duration 49 | } 50 | -------------------------------------------------------------------------------- /oidc/provider/cookie.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package provider 19 | 20 | import ( 21 | "net/http" 22 | ) 23 | 24 | func (p *Provider) setBrowserStateCookie(rw http.ResponseWriter, value string) error { 25 | cookie := http.Cookie{ 26 | Name: p.browserStateCookieName, 27 | Value: value, 28 | 29 | Path: p.browserStateCookiePath, 30 | Secure: true, 31 | HttpOnly: false, // This Cookie is intended to be read by Javascript. 32 | SameSite: http.SameSiteNoneMode, 33 | } 34 | http.SetCookie(rw, &cookie) 35 | 36 | return nil 37 | } 38 | 39 | func (p *Provider) removeBrowserStateCookie(rw http.ResponseWriter) error { 40 | cookie := http.Cookie{ 41 | Name: p.browserStateCookieName, 42 | 43 | Path: p.browserStateCookiePath, 44 | Secure: true, 45 | HttpOnly: false, // This Cookie is intended to be read by Javascript. 46 | SameSite: http.SameSiteNoneMode, 47 | 48 | Expires: farPastExpiryTime, 49 | } 50 | http.SetCookie(rw, &cookie) 51 | 52 | return nil 53 | } 54 | 55 | func (p *Provider) setSessionCookie(rw http.ResponseWriter, value string) error { 56 | cookie := http.Cookie{ 57 | Name: p.sessionCookieName, 58 | Value: value, 59 | 60 | Path: p.sessionCookiePath, 61 | Secure: true, 62 | HttpOnly: true, 63 | SameSite: http.SameSiteNoneMode, 64 | } 65 | http.SetCookie(rw, &cookie) 66 | 67 | return nil 68 | } 69 | 70 | func (p *Provider) getSessionCookie(req *http.Request) (string, error) { 71 | cookie, err := req.Cookie(p.sessionCookieName) 72 | if err != nil { 73 | return "", err 74 | } 75 | 76 | return cookie.Value, nil 77 | } 78 | 79 | func (p *Provider) removeSessionCookie(rw http.ResponseWriter) error { 80 | cookie := http.Cookie{ 81 | Name: p.sessionCookieName, 82 | 83 | Path: p.sessionCookiePath, 84 | Secure: true, 85 | HttpOnly: true, 86 | SameSite: http.SameSiteNoneMode, 87 | 88 | Expires: farPastExpiryTime, 89 | } 90 | http.SetCookie(rw, &cookie) 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /oidc/provider/identity.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package provider 19 | 20 | import ( 21 | "errors" 22 | 23 | "github.com/dgrijalva/jwt-go" 24 | 25 | "stash.kopano.io/kc/konnect/identity" 26 | "stash.kopano.io/kc/konnect/oidc/payload" 27 | ) 28 | 29 | func (p *Provider) getIdentityManager(identityProvider string) (identity.Manager, error) { 30 | if identityProvider == "" { 31 | // Return default manager when empty (backwards compatibility). 32 | return p.identityManager, nil 33 | } 34 | 35 | if identityProvider == p.identityManager.Name() { 36 | return p.identityManager, nil 37 | } 38 | if p.guestManager != nil && identityProvider == p.guestManager.Name() { 39 | return p.guestManager, nil 40 | } 41 | 42 | return nil, errors.New("identity provider mismatch") 43 | } 44 | 45 | func (p *Provider) getIdentityManagerFromClaims(identityProvider string, identityClaims jwt.MapClaims) (identity.Manager, error) { 46 | if identityClaims == nil { 47 | // Return default manager when no claims. 48 | return p.identityManager, nil 49 | } 50 | 51 | return p.getIdentityManager(identityProvider) 52 | } 53 | 54 | func (p *Provider) getIdentityManagerFromSession(session *payload.Session) (identity.Manager, error) { 55 | if session == nil { 56 | // Return default manager when no session. 57 | return p.identityManager, nil 58 | } 59 | 60 | return p.getIdentityManager(session.Provider) 61 | } 62 | -------------------------------------------------------------------------------- /oidc/provider/signing.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package provider 19 | 20 | import ( 21 | "crypto" 22 | 23 | "github.com/dgrijalva/jwt-go" 24 | ) 25 | 26 | // A SigningKey bundles a signer with meta data and a signign method. 27 | type SigningKey struct { 28 | ID string 29 | PrivateKey crypto.Signer 30 | SigningMethod jwt.SigningMethod 31 | } 32 | -------------------------------------------------------------------------------- /oidc/provider/state.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package provider 19 | 20 | import ( 21 | "crypto/sha256" 22 | "encoding/base64" 23 | "encoding/hex" 24 | "fmt" 25 | "net/http" 26 | "net/url" 27 | 28 | "golang.org/x/crypto/blake2b" 29 | "stash.kopano.io/kgol/rndm" 30 | 31 | "stash.kopano.io/kc/konnect/identity" 32 | "stash.kopano.io/kc/konnect/oidc/payload" 33 | ) 34 | 35 | var browserStateMarker = []byte("kopano-konnect-1") 36 | 37 | func (p *Provider) makeBrowserState(ar *payload.AuthenticationRequest, auth identity.AuthRecord, err error) (string, error) { 38 | hasher, hasherErr := blake2b.New256(nil) 39 | if hasherErr != nil { 40 | return "", hasherErr 41 | } 42 | if auth != nil && err == nil { 43 | hasher.Write([]byte(auth.Subject())) 44 | } else { 45 | // Use empty string value when not signed in or with error. This means 46 | // that a browser state is always created. 47 | hasher.Write([]byte(" ")) 48 | } 49 | hasher.Write([]byte(" ")) 50 | hasher.Write([]byte(p.issuerIdentifier)) 51 | hasher.Write([]byte(" ")) 52 | hasher.Write(browserStateMarker) 53 | 54 | // Encode to string. 55 | browserState := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil)) 56 | 57 | return browserState, nil 58 | } 59 | 60 | func (p *Provider) makeSessionState(req *http.Request, ar *payload.AuthenticationRequest, browserState string) (string, error) { 61 | var origin string 62 | 63 | for { 64 | redirectURL := ar.RedirectURI 65 | if redirectURL != nil { 66 | origin = fmt.Sprintf("%s://%s", redirectURL.Scheme, redirectURL.Host) 67 | break 68 | } 69 | 70 | originHeader := req.Header.Get("Origin") 71 | if originHeader != "" { 72 | origin = originHeader 73 | break 74 | } 75 | 76 | refererHeader := req.Header.Get("Referer") 77 | if refererHeader != "" { 78 | // Rescure from referer. 79 | refererURL, err := url.Parse(refererHeader) 80 | if err != nil { 81 | return "", fmt.Errorf("invalid referer value: %v", err) 82 | } 83 | 84 | origin = fmt.Sprintf("%s://%s", refererURL.Scheme, refererURL.Host) 85 | break 86 | } 87 | 88 | return "", fmt.Errorf("missing origin") 89 | } 90 | 91 | salt := rndm.GenerateRandomString(32) 92 | 93 | hasher := sha256.New() 94 | hasher.Write([]byte(ar.ClientID)) 95 | hasher.Write([]byte(" ")) 96 | hasher.Write([]byte(origin)) 97 | hasher.Write([]byte(" ")) 98 | hasher.Write([]byte(browserState)) 99 | hasher.Write([]byte(" ")) 100 | hasher.Write([]byte(salt)) 101 | 102 | sessionState := fmt.Sprintf("%s.%s", hex.EncodeToString(hasher.Sum(nil)), salt) 103 | 104 | return sessionState, nil 105 | } 106 | -------------------------------------------------------------------------------- /oidc/provider/subject.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package provider 19 | 20 | import ( 21 | "errors" 22 | 23 | "stash.kopano.io/kc/konnect" 24 | "stash.kopano.io/kc/konnect/identity" 25 | ) 26 | 27 | // PublicSubjectFromAuth creates the provideds auth Subject value with the 28 | // accociated provider. This subject can be used as URL safe value to uniquely 29 | // identify the provided auth user with remote systems. 30 | func (p *Provider) PublicSubjectFromAuth(auth identity.AuthRecord) (string, error) { 31 | authorizedScopes := auth.AuthorizedScopes() 32 | if ok, _ := authorizedScopes[konnect.ScopeRawSubject]; ok { 33 | // Return raw subject as is when with ScopeRawSubject. 34 | user := auth.User() 35 | if user == nil { 36 | return "", errors.New("no user") 37 | } 38 | 39 | return user.Raw(), nil 40 | } 41 | 42 | return auth.Subject(), nil 43 | } 44 | -------------------------------------------------------------------------------- /oidc/provider/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package provider 19 | 20 | import ( 21 | "fmt" 22 | "net/http" 23 | "net/url" 24 | "time" 25 | ) 26 | 27 | var ( 28 | farPastExpiryTime = time.Unix(0, 0) 29 | ) 30 | 31 | func uniqueStrings(s []string) []string { 32 | var res []string 33 | m := make(map[string]bool) 34 | for _, s := range s { 35 | if _, ok := m[s]; ok { 36 | continue 37 | } 38 | m[s] = true 39 | res = append(res, s) 40 | } 41 | 42 | return res 43 | } 44 | 45 | func getRequestURL(req *http.Request, isTrustedSource bool) *url.URL { 46 | u, _ := url.Parse(req.URL.String()) 47 | 48 | if isTrustedSource { 49 | // Look at proxy injected values to rewrite URLs if trusted. 50 | for { 51 | prefix := req.Header.Get("X-Forwarded-Prefix") 52 | if prefix != "" { 53 | u.Path = fmt.Sprintf("%s%s", prefix, u.Path) 54 | break 55 | } 56 | replaced := req.Header.Get("X-Replaced-Path") 57 | if replaced != "" { 58 | u.Path = replaced 59 | break 60 | } 61 | 62 | break 63 | } 64 | } 65 | 66 | return u 67 | } 68 | 69 | func addResponseHeaders(header http.Header) { 70 | header.Set("Cache-Control", "no-cache, no-store, must-revalidate") 71 | header.Set("Pragma", "no-cache") 72 | header.Set("X-Content-Type-Options", "nosniff") 73 | header.Set("Referrer-Policy", "origin") 74 | } 75 | 76 | func makeArrayFromBoolMap(m map[string]bool) []string { 77 | result := []string{} 78 | for k, v := range m { 79 | if v { 80 | result = append(result, k) 81 | } 82 | } 83 | 84 | return result 85 | } 86 | -------------------------------------------------------------------------------- /payloads.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package konnect 19 | 20 | import ( 21 | "stash.kopano.io/kc/konnect/oidc/payload" 22 | ) 23 | 24 | // UserInfoResponse defines the data returned from the Konnect UserInfo 25 | // endpoint. It is the standard ODIC response, extended with additional claims. 26 | type UserInfoResponse struct { 27 | *payload.UserInfoResponse 28 | 29 | *IDClaims 30 | *UniqueUserIDClaims 31 | } 32 | -------------------------------------------------------------------------------- /provider.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package konnect 19 | 20 | import ( 21 | "context" 22 | 23 | "stash.kopano.io/kc/konnect/identity" 24 | ) 25 | 26 | // AccessTokenProvider is an interface for something which can create 27 | // access tokens. 28 | type AccessTokenProvider interface { 29 | MakeAccessToken(ctx context.Context, audience string, auth identity.AuthRecord) (string, error) 30 | } 31 | -------------------------------------------------------------------------------- /scopes.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package konnect 19 | 20 | const ( 21 | // ScopeID is the string value for the Konnect ID scope. 22 | ScopeID = "konnect/id" 23 | // ScopeUniqueUserID is the string value for the Konnect Unique User ID scope. 24 | ScopeUniqueUserID = "konnect/uuid" 25 | 26 | // ScopeRawSubject is the string value for the Konnect Raw Subject scope. 27 | ScopeRawSubject = "konnect/raw_sub" 28 | 29 | // ScopeGuestOK is the string value for the Konnect Guest OK scope. 30 | ScopeGuestOK = "konnect/guestok" 31 | ) 32 | -------------------------------------------------------------------------------- /scopes.yaml.in: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | mapping: 4 | # old-scope: custom-scope 5 | 6 | scopes: 7 | # custom-scope: 8 | # description: "This is the a custom scope" 9 | # priority: 100 10 | 11 | # another-scope: 12 | # description: "This is the another scope" 13 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Scripts and helpers for Kopano Konnect 2 | 3 | This folder contains various scripts and helpers to integrate Konnect with 4 | various ways how it can be started and configured. 5 | 6 | ## Rkt 7 | 8 | Rkt is a pod-native container engine for Linux. Here are some examples how to 9 | integrate/run konnect with rkt. The following section is kept here as example. 10 | 11 | ### Config file as systemd environment file 12 | 13 | ``` 14 | # OpenID Connect Issuer Identifier 15 | ISS=https://konnect.local 16 | 17 | # Full file path to a PEM encoded PKCS#1 or PKCS#5 private key which is used to 18 | # sign tokens. This file must exist and be valid to be able to start the 19 | # service. A suitable key can be generated with: 20 | # `openssl genpkey -algorithm RSA \ 21 | # -out konnectd-signing-private-key.pem.pem \ 22 | # -pkeyopt rsa_keygen_bits:4096` 23 | SIGNING_PRIVATE_KEY=/etc/kopano/konnectd-signing-private-key.pem 24 | 25 | # Full file path to a encryption secret key file containing random bytes. This 26 | # file must exist to be able to start the service. A suitable file can be 27 | # generated with: 28 | # `openssl rand -out konnectd-encryption.key 32` 29 | ENCRYPTION_SECRET=/etc/kopano/konnectd-encryption.key 30 | 31 | # Full file path to the identifier registration configuration file. This file 32 | # must exist to be able to start the service. An example file is shipped with 33 | # the documentation / sources. 34 | IDENTIFIER_REGISTRATION_CONF=/etc/kopano/konnectd-identifier-registration.yaml 35 | 36 | # Identity manager Kopano Core via direct connection. 37 | IDENTITY_MANAGER=kc 38 | KOPANO_SERVER_DEFAULT_URI=file:///run/kopano-server.sock 39 | KOPANO_SERVER_USERNAME=SYSTEM 40 | KOPANO_SERVER_PASSWORD= 41 | 42 | # Identity manager Kopano Webapp via Cookie pass through. 43 | #SIGN_IN_URL=https://your-kopano.local/webapp/index.php 44 | #IDENTITY_MANAGER=cookie "$SIGN_IN_URL?load=custom&name=oidcuser" KOPANO_WEBAPP encryption-store-key 45 | 46 | #IMAGE=/srv/images/kopano-konnectd-latest-linux-amd64.aci 47 | #RKT_ARGS=--port=www:127.0.0.1:8777 48 | #ARGS=--insecure 49 | ``` 50 | 51 | ### Systemd service with rkt 52 | 53 | ``` 54 | [Unit] 55 | Description=kopano-konnectd 56 | Requires=network-online.target 57 | After=network-online.target 58 | 59 | [Service] 60 | Slice=machine.slice 61 | Environment=RKT_ARGS=--port=www:127.0.0.1:8777 62 | Environment=ISS=https://konnect.local 63 | Environment=SIGNING_PRIVATE_KEY=/etc/kopano/konnectd-tokens-signing-key.pem 64 | Environment=ENCRYPTION_SECRET=/etc/kopano/konnectd-encryption.key 65 | Environment=IDENTIFIER_REGISTRATION_CONF=/etc/kopano/konnectd-identifier-registration.yaml 66 | Environment=IMAGE=/srv/images/kopano-konnectd-latest-linux-amd64.aci 67 | EnvironmentFile=/etc/default/kopano-konnectd 68 | ExecStart=/usr/bin/rkt \ 69 | --insecure-options=image \ 70 | run $RKT_ARGS \ 71 | --volume signing-private-key,kind=host,source=${SIGNING_PRIVATE_KEY} \ 72 | --volume encryption-secret,kind=host,source=${ENCRYPTION_SECRET} \ 73 | --volume identifier-registration-conf,kind=host,source=${IDENTIFIER_REGISTRATION_CONF} \ 74 | --volume etc-ssl-certs,kind=host,source=/etc/ssl/certs \ 75 | --volume run,kind=host,source=/run \ 76 | ${IMAGE} \ 77 | --environment=KOPANO_SERVER_DEFAULT_URI="$KOPANO_SERVER_DEFAULT_URI" \ 78 | --environment=KOPANO_SERVER_USERNAME="$KOPANO_SERVER_USERNAME" \ 79 | --environment=KOPANO_SERVER_PASSWORD="$KOPANO_SERVER_PASSWORD" \ 80 | -- \ 81 | --iss=${ISS} \ 82 | --sign-in-uri=${SIGN_IN_URL} \ 83 | $ARGS $IDENTITY_MANAGER 84 | ExecStopPost=/usr/bin/rkt gc --mark-only 85 | KillMode=mixed 86 | Restart=always 87 | ``` 88 | -------------------------------------------------------------------------------- /scripts/build-konnectd-aci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BINARY=bin/konnectd 4 | IDENTIFIER_WEBAPP_BUILD=identifier/build 5 | HOSTOS=$(go env GOHOSTOS) 6 | ARCH=$(go env GOHOSTARCH) 7 | 8 | make 9 | VERSION=$($BINARY version|grep Version|awk -F': ' '{ print $2 }') 10 | (cd identifier && make) 11 | 12 | acbuild --debug begin 13 | trap "{ export EXT=$?; acbuild --debug end && rm -rf $TMPDIR && exit $EXT; }" EXIT 14 | 15 | acbuild --debug set-name kopano.com/konnectd 16 | acbuild --debug set-user 301 17 | acbuild --debug set-group 301 18 | acbuild --debug copy $BINARY /srv/konnectd 19 | acbuild --debug copy $IDENTIFIER_WEBAPP_BUILD /srv/identifier-webapp 20 | acbuild --debug set-exec -- /srv/konnectd serve \ 21 | --listen=0.0.0.0:8777 \ 22 | --signing-private-key=/signing-private-key.pem \ 23 | --encryption-secret=/encryption.key \ 24 | --identifier-registration-conf=/identifier-registration.yaml \ 25 | --identifier-client-path=/srv/identifier-webapp 26 | acbuild --debug port add www tcp 8777 27 | acbuild --debug mount add signing-private-key /signing-private-key.pem --read-only 28 | acbuild --debug mount add encryption-secret /encryption.key --read-only 29 | acbuild --debug mount add identifier-registration-conf /identifier-registration.yaml --read-only 30 | acbuild --debug mount add etc-ssl-certs /etc/ssl/certs --read-only 31 | acbuild --debug mount add run /run 32 | acbuild --debug label add version $VERSION 33 | acbuild --debug label add arch $ARCH 34 | acbuild --debug label add os $HOSTOS 35 | acbuild --debug write --overwrite kopano-konnectd-$VERSION-$HOSTOS-$ARCH.aci 36 | -------------------------------------------------------------------------------- /scripts/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright 2017-2019 Kopano and its licensors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | set -euo pipefail 19 | 20 | # Check for parameters, prepend with our exe when the first arg is a parameter. 21 | if [ "${1:0:1}" = '-' ]; then 22 | set -- ${EXE} "$@" 23 | else 24 | # Check for some basic commands, this is used to allow easy calling without 25 | # having to prepend the binary all the time. 26 | case "${1}" in 27 | help|version) 28 | set -- ${EXE} "$@" 29 | ;; 30 | 31 | serve) 32 | shift 33 | set -- ${EXE} serve --metrics-listen=0.0.0.0:6777 "$@" 34 | ;; 35 | esac 36 | fi 37 | 38 | # Setup environment. 39 | setup_env() { 40 | [ -f /etc/defaults/docker-env ] && source /etc/defaults/docker-env 41 | } 42 | setup_env 43 | 44 | # Support additional args provided via environment. 45 | if [ -n "${ARGS}" ]; then 46 | set -- "$@" ${ARGS} 47 | fi 48 | 49 | # Run the service, optionally switching user when running as root. 50 | if [ $(id -u) = 0 -a -n "${KONNECTD_USER}" ]; then 51 | userAndgroup="${KONNECTD_USER}" 52 | if [ -n "${KONNECTD_GROUP}" ]; then 53 | userAndgroup="${userAndgroup}:${KONNECTD_GROUP}" 54 | fi 55 | exec su-exec ${userAndgroup} "$@" 56 | else 57 | exec "$@" 58 | fi 59 | -------------------------------------------------------------------------------- /scripts/healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright 2017-2019 Kopano and its licensors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | set -euo pipefail 19 | 20 | set -- ${EXE} healthcheck "$@" 21 | 22 | exec "$@" 23 | -------------------------------------------------------------------------------- /scripts/kopano-konnectd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Kopano Konnect Daemon 3 | 4 | [Service] 5 | Type=simple 6 | PrivateTmp=yes 7 | User=konnect 8 | Group=kopano 9 | NoNewPrivileges=yes 10 | PrivateUsers=yes 11 | CapabilityBoundingSet= 12 | ProtectSystem=full 13 | UMask=0077 14 | LimitNOFILE=infinity 15 | PermissionsStartOnly=true 16 | Environment=LC_CTYPE=en_US.UTF-8 17 | EnvironmentFile=-/etc/kopano/konnectd.cfg 18 | ExecStartPre=/usr/sbin/kopano-konnectd setup 19 | ExecStart=/usr/sbin/kopano-konnectd serve --log-timestamp=false 20 | 21 | [Install] 22 | WantedBy=multi-user.target 23 | -------------------------------------------------------------------------------- /server/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package server 19 | 20 | import ( 21 | "context" 22 | "net/http" 23 | 24 | "github.com/gorilla/mux" 25 | 26 | "stash.kopano.io/kc/konnect/config" 27 | ) 28 | 29 | // Config defines a Server's configuration settings. 30 | type Config struct { 31 | Config *config.Config 32 | 33 | Handler http.Handler 34 | Routes []WithRoutes 35 | } 36 | 37 | // WithRoutes provide http routing within a context. 38 | type WithRoutes interface { 39 | AddRoutes(ctx context.Context, router *mux.Router) 40 | } 41 | -------------------------------------------------------------------------------- /server/handlers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package server 19 | 20 | import ( 21 | "net/http" 22 | ) 23 | 24 | // HealthCheckHandler a http handler return 200 OK when server health is fine. 25 | func (s *Server) HealthCheckHandler(rw http.ResponseWriter, req *http.Request) { 26 | rw.WriteHeader(http.StatusOK) 27 | } 28 | -------------------------------------------------------------------------------- /server/handlers_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package server 19 | 20 | import ( 21 | "context" 22 | "net/http" 23 | "net/http/httptest" 24 | "testing" 25 | ) 26 | 27 | func TestHealthCheckHandler(t *testing.T) { 28 | ctx, cancel := context.WithCancel(context.Background()) 29 | defer cancel() 30 | 31 | // Create our server. 32 | httpServer, _, router, _ := newTestServer(ctx, t) 33 | defer httpServer.Close() 34 | 35 | // Prepare the request to pass to our handler. 36 | req, err := http.NewRequest("GET", "/health-check", nil) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | // Create response recorder to record the response. 42 | rr := httptest.NewRecorder() 43 | router.ServeHTTP(rr, req) 44 | 45 | // Check the status code is what we expect. 46 | if status := rr.Code; status != http.StatusOK { 47 | t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package server 19 | 20 | import ( 21 | "context" 22 | "net/http" 23 | "net/http/httptest" 24 | "os" 25 | "testing" 26 | 27 | "github.com/gorilla/mux" 28 | "github.com/sirupsen/logrus" 29 | 30 | "stash.kopano.io/kc/konnect/config" 31 | "stash.kopano.io/kc/konnect/identity" 32 | "stash.kopano.io/kc/konnect/identity/clients" 33 | identityManagers "stash.kopano.io/kc/konnect/identity/managers" 34 | "stash.kopano.io/kc/konnect/managers" 35 | codeManagers "stash.kopano.io/kc/konnect/oidc/code/managers" 36 | "stash.kopano.io/kc/konnect/oidc/provider" 37 | ) 38 | 39 | var logger = &logrus.Logger{ 40 | Out: os.Stderr, 41 | Formatter: &logrus.TextFormatter{DisableColors: true}, 42 | Level: logrus.DebugLevel, 43 | } 44 | 45 | func newTestServer(ctx context.Context, t *testing.T) (*httptest.Server, *Server, http.Handler, *config.Config) { 46 | mgrs := managers.New() 47 | mgrs.Set("identity", identityManagers.NewDummyIdentityManager( 48 | &identity.Config{}, 49 | "unittestuser", 50 | )) 51 | mgrs.Set("code", codeManagers.NewMemoryMapManager(ctx)) 52 | encryptionManager, _ := identityManagers.NewEncryptionManager(nil) 53 | mgrs.Set("encryption", encryptionManager) 54 | mgrs.Set("clients", &clients.Registry{}) 55 | 56 | cfg := &config.Config{ 57 | Logger: logger, 58 | } 59 | 60 | p, err := provider.NewProvider(&provider.Config{ 61 | Config: cfg, 62 | 63 | IssuerIdentifier: "http://localhost:8777", 64 | WellKnownPath: "/.well-known/openid-configuration", 65 | JwksPath: "/konnect/v1/jwks.json", 66 | AuthorizationPath: "/konnect/v1/authorize", 67 | TokenPath: "/konnect/v1/token", 68 | UserInfoPath: "/konnect/v1/userinfo", 69 | }) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | err = p.RegisterManagers(mgrs) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | server, err := NewServer(&Config{ 79 | Config: cfg, 80 | 81 | Handler: p, 82 | }) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | router := mux.NewRouter() 87 | server.AddRoutes(ctx, router) 88 | 89 | s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 90 | router.ServeHTTP(rw, req) 91 | })) 92 | 93 | return s, server, router, cfg 94 | } 95 | 96 | func TestNewTestServer(t *testing.T) { 97 | ctx, cancel := context.WithCancel(context.Background()) 98 | defer cancel() 99 | newTestServer(ctx, t) 100 | } 101 | -------------------------------------------------------------------------------- /signing/jwk.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package signing 19 | 20 | import ( 21 | "crypto" 22 | "encoding/base64" 23 | 24 | jwk "github.com/mendsley/gojwk" 25 | "golang.org/x/crypto/ed25519" 26 | ) 27 | 28 | // JWKFromPublicKey creates a JWK from a public key 29 | func JWKFromPublicKey(key crypto.PublicKey) (*jwk.Key, error) { 30 | switch key := key.(type) { 31 | case ed25519.PublicKey: 32 | jwt := &jwk.Key{ 33 | Kty: "OKP", 34 | Crv: "Ed25519", 35 | X: base64.RawURLEncoding.EncodeToString(key), 36 | } 37 | 38 | return jwt, nil 39 | 40 | default: 41 | return jwk.PublicKey(key) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /signing/jwt.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package signing 19 | 20 | import ( 21 | "crypto" 22 | "crypto/rand" 23 | "errors" 24 | 25 | "github.com/dgrijalva/jwt-go" 26 | "golang.org/x/crypto/ed25519" 27 | ) 28 | 29 | // Errors used by this package. 30 | var ( 31 | ErrEdDSAVerification = errors.New("eddsa: verification error") 32 | ) 33 | 34 | // SigningMethodEdwardsCurve implements the EdDSA family of signing methods. 35 | type SigningMethodEdwardsCurve struct { 36 | Name string 37 | } 38 | 39 | // Specific instances for EdDSA 40 | var ( 41 | SigningMethodEdDSA *SigningMethodEdwardsCurve 42 | ) 43 | 44 | func init() { 45 | // EdDSA with Ed25519 https://tools.ietf.org/html/rfc8037#section-3.1. 46 | SigningMethodEdDSA = &SigningMethodEdwardsCurve{"EdDSA"} 47 | jwt.RegisterSigningMethod(SigningMethodEdDSA.Alg(), func() jwt.SigningMethod { 48 | return SigningMethodEdDSA 49 | }) 50 | } 51 | 52 | // Alg implements the jwt.SigningMethod interface. 53 | func (m *SigningMethodEdwardsCurve) Alg() string { 54 | return m.Name 55 | } 56 | 57 | // Verify implements the jwt.SigningMethod interface. 58 | func (m *SigningMethodEdwardsCurve) Verify(signingString, signature string, key interface{}) error { 59 | var err error 60 | 61 | // Decode the signature 62 | var sig []byte 63 | if sig, err = jwt.DecodeSegment(signature); err != nil { 64 | return err 65 | } 66 | 67 | // Get the key 68 | switch k := key.(type) { 69 | case ed25519.PublicKey: 70 | if len(k) != ed25519.PublicKeySize { 71 | return jwt.ErrInvalidKey 72 | } 73 | if verifystatus := ed25519.Verify(k, []byte(signingString), sig); verifystatus == true { 74 | return nil 75 | } else { 76 | return ErrEdDSAVerification 77 | } 78 | 79 | default: 80 | return jwt.ErrInvalidKeyType 81 | } 82 | } 83 | 84 | // Sign implements the jwt.SigningMethod interface. 85 | func (m *SigningMethodEdwardsCurve) Sign(signingString string, key interface{}) (string, error) { 86 | switch k := key.(type) { 87 | case ed25519.PrivateKey: 88 | if len(k) != ed25519.PrivateKeySize { 89 | return "", jwt.ErrInvalidKey 90 | } 91 | if s, err := k.Sign(rand.Reader, []byte(signingString), crypto.Hash(0)); err == nil { 92 | // We serialize the signature. 93 | return jwt.EncodeSegment(s), nil 94 | } else { 95 | return "", err 96 | } 97 | 98 | default: 99 | return "", jwt.ErrInvalidKeyType 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /utils/errorpage.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "fmt" 22 | "net/http" 23 | ) 24 | 25 | // WriteErrorPage create a formatted error page response containing the provided 26 | // information and writes it to the provided http.ResponseWriter. 27 | func WriteErrorPage(rw http.ResponseWriter, code int, title string, message string) { 28 | if title == "" { 29 | title = http.StatusText(code) 30 | } 31 | 32 | text := fmt.Sprintf("%d %s", code, title) 33 | if message != "" { 34 | text = text + " - " + message 35 | } 36 | 37 | http.Error(rw, text, code) 38 | } 39 | -------------------------------------------------------------------------------- /utils/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "fmt" 22 | ) 23 | 24 | // ErrorWithDescription is an interface binding the standard error 25 | // inteface with a description. 26 | type ErrorWithDescription interface { 27 | error 28 | Description() string 29 | } 30 | 31 | // DescribeError returns a wrapped version for errors which contain additional 32 | // fields. The wrapped version contains all fields as a string value. Use this 33 | // for general purpose logging of rich errors. 34 | func DescribeError(err error) error { 35 | switch err.(type) { 36 | case ErrorWithDescription: 37 | err = fmt.Errorf("%s - %s", err, err.(ErrorWithDescription).Description()) 38 | } 39 | 40 | return err 41 | } 42 | 43 | // ErrorAsFields returns a mapping of all fields of the provided error. 44 | func ErrorAsFields(err error) map[string]interface{} { 45 | if err == nil { 46 | return nil 47 | } 48 | 49 | fields := make(map[string]interface{}) 50 | fields["error"] = err.Error() 51 | switch err.(type) { 52 | case ErrorWithDescription: 53 | fields["desc"] = err.(ErrorWithDescription).Description() 54 | } 55 | 56 | return fields 57 | } 58 | -------------------------------------------------------------------------------- /utils/http.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "crypto/tls" 22 | "net" 23 | "net/http" 24 | "time" 25 | 26 | "golang.org/x/net/http2" 27 | 28 | "stash.kopano.io/kc/konnect/version" 29 | ) 30 | 31 | const ( 32 | defaultHTTPTimeout = 30 * time.Second 33 | defaultHTTPKeepAlive = 30 * time.Second 34 | defaultHTTPMaxIdleConns = 100 35 | defaultHTTPIdleConnTimeout = 90 * time.Second 36 | defaultHTTPTLSHandshakeTimeout = 10 * time.Second 37 | defaultHTTPExpectContinueTimeout = 1 * time.Second 38 | ) 39 | 40 | // DefaultHTTPUserAgent is the User-Agent Header which should be used when 41 | // making HTTP requests. 42 | var DefaultHTTPUserAgent = "Kopano-Konnect/" + version.Version 43 | 44 | // HTTPTransportWithTLSClientConfig creates a new http.Transport with sane 45 | // default settings using the provided tls.Config. 46 | func HTTPTransportWithTLSClientConfig(tlsClientConfig *tls.Config) *http.Transport { 47 | transport := &http.Transport{ 48 | Proxy: http.ProxyFromEnvironment, 49 | DialContext: (&net.Dialer{ 50 | Timeout: defaultHTTPTimeout, 51 | KeepAlive: defaultHTTPKeepAlive, 52 | DualStack: true, 53 | }).DialContext, 54 | MaxIdleConns: defaultHTTPMaxIdleConns, 55 | IdleConnTimeout: defaultHTTPIdleConnTimeout, 56 | TLSHandshakeTimeout: defaultHTTPTLSHandshakeTimeout, 57 | ExpectContinueTimeout: defaultHTTPExpectContinueTimeout, 58 | } 59 | if tlsClientConfig != nil { 60 | transport.TLSClientConfig = tlsClientConfig 61 | err := http2.ConfigureTransport(transport) 62 | if err != nil { 63 | panic(err) 64 | } 65 | } 66 | 67 | return transport 68 | } 69 | 70 | // DefaultTLSConfig returns a new tls.Config. 71 | func DefaultTLSConfig() *tls.Config { 72 | return &tls.Config{ 73 | ClientSessionCache: tls.NewLRUClientSessionCache(0), 74 | } 75 | } 76 | 77 | // InsecureSkipVerifyTLSConfig returns a new tls.Config which does skip TLS verification. 78 | func InsecureSkipVerifyTLSConfig() *tls.Config { 79 | config := DefaultTLSConfig() 80 | config.InsecureSkipVerify = true 81 | 82 | return config 83 | } 84 | 85 | // DefaultHTTPClient is a http.Client with a timeout set. 86 | var DefaultHTTPClient = &http.Client{ 87 | Timeout: defaultHTTPTimeout, 88 | Transport: HTTPTransportWithTLSClientConfig(DefaultTLSConfig()), 89 | } 90 | 91 | // InsecureHTTPClient is a http.Client with a timeout set and with TLS 92 | // verification disabled. 93 | var InsecureHTTPClient = &http.Client{ 94 | Timeout: defaultHTTPTimeout, 95 | Transport: HTTPTransportWithTLSClientConfig(InsecureSkipVerifyTLSConfig()), 96 | } 97 | -------------------------------------------------------------------------------- /utils/json.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "encoding/json" 22 | "net/http" 23 | ) 24 | 25 | const ( 26 | defaultJSONContentType = "application/json; encoding=utf-8" 27 | ) 28 | 29 | // WriteJSON marshals the provided data as JSON and writes it to the provided 30 | // http.ResponseWriter using the provided HTTP status code and content-type. the 31 | // nature of this function is that it always writes a HTTP response header. Thus 32 | // it makes no sense to write another header on error. Resulting errors should 33 | // be logged and the connection should be closes as it is non-functional. 34 | func WriteJSON(rw http.ResponseWriter, code int, data interface{}, contentType string) error { 35 | if contentType == "" { 36 | rw.Header().Set("Content-Type", defaultJSONContentType) 37 | } else { 38 | rw.Header().Set("content-Type", contentType) 39 | } 40 | 41 | rw.WriteHeader(code) 42 | 43 | enc := json.NewEncoder(rw) 44 | enc.SetIndent("", " ") 45 | 46 | return enc.Encode(data) 47 | } 48 | -------------------------------------------------------------------------------- /utils/origin.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "net/http" 22 | "net/url" 23 | ) 24 | 25 | // OriginFromRequestHeaders tries to find information about the origin from the 26 | // provided http.Header. It first looks into the Origin header field and if that 27 | // is not found it looks into the Referer header field. If both are not found 28 | // an empty string is returned. 29 | func OriginFromRequestHeaders(header http.Header) string { 30 | origin := header.Get("Origin") 31 | if origin == "" { 32 | referer := header.Get("Referer") 33 | if referer != "" { 34 | refererURI, _ := url.Parse(referer) 35 | origin = refererURI.Scheme + "://" + refererURI.Host 36 | } 37 | } 38 | 39 | return origin 40 | } 41 | -------------------------------------------------------------------------------- /utils/redirect.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "net/http" 22 | "net/url" 23 | "strings" 24 | 25 | "github.com/google/go-querystring/query" 26 | ) 27 | 28 | // WriteRedirect crates a URL out of the provided uri and params and writes a 29 | // a HTTP response with the provided HTTP status code to the provided 30 | // http.ResponseWriter incliding HTTP caching headers to prevent caching. If 31 | // asFragment is true, the provided params are added as URL fragment, otherwise 32 | // they replace the query. If params is nil, the provided uri is taken as is. 33 | func WriteRedirect(rw http.ResponseWriter, code int, uri *url.URL, params interface{}, asFragment bool) error { 34 | if params != nil { 35 | paramValues, err := query.Values(params) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | u, _ := url.Parse(uri.String()) 41 | if asFragment { 42 | if u.Fragment != "" { 43 | u.Fragment += "&" 44 | } 45 | f := paramValues.Encode() // This encods into URL encoded form with QueryEscape. 46 | f, _ = url.QueryUnescape(f) // But we need it unencoded since its the fragment, it is encoded later (when serializing the URL). 47 | u.Fragment += f // Append fragment extension. 48 | } else { 49 | queryValues := u.Query() 50 | for k, vs := range paramValues { 51 | for _, v := range vs { 52 | queryValues.Add(k, v) 53 | } 54 | } 55 | u.RawQuery = strings.ReplaceAll(queryValues.Encode(), "+", "%20") // NOTE(longsleep): Ensure we use %20 instead of +. 56 | } 57 | uri = u 58 | } 59 | 60 | rw.Header().Set("Location", uri.String()) 61 | rw.Header().Set("Cache-Control", "no-store") 62 | rw.Header().Set("Pragma", "no-cache") 63 | 64 | rw.WriteHeader(code) 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /utils/schema.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "github.com/gorilla/schema" 22 | ) 23 | 24 | // Create a Decoder instance as a package global, because it caches 25 | // meta-data about structs, and an instance can be shared safely. 26 | var urlSchemaDecoder = schema.NewDecoder() 27 | 28 | // DecodeURLSchema decodes request for mdata in to the provided dst url struct. 29 | func DecodeURLSchema(dst interface{}, src map[string][]string) error { 30 | return urlSchemaDecoder.Decode(dst, src) 31 | } 32 | 33 | func init() { 34 | urlSchemaDecoder.SetAliasTag("url") 35 | urlSchemaDecoder.IgnoreUnknownKeys(true) 36 | } 37 | -------------------------------------------------------------------------------- /utils/trusted.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "net" 22 | "net/http" 23 | ) 24 | 25 | // IsRequestFromTrustedSource checks if the provided requests remote address is 26 | // one either one of the provided ips or in one of the provided networks. 27 | func IsRequestFromTrustedSource(req *http.Request, ips []*net.IP, nets []*net.IPNet) (bool, error) { 28 | ipString, _, err := net.SplitHostPort(req.RemoteAddr) 29 | if err != nil { 30 | return false, err 31 | } 32 | 33 | ip := net.ParseIP(ipString) 34 | 35 | for _, checkIP := range ips { 36 | if checkIP.Equal(ip) { 37 | return true, nil 38 | } 39 | } 40 | 41 | for _, checkNet := range nets { 42 | if checkNet.Contains(ip) { 43 | return true, nil 44 | } 45 | } 46 | 47 | return false, nil 48 | } 49 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package utils 19 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Kopano and its licensors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package version 19 | 20 | // BuildDate defines the date when build/compile was run. This will be filled in 21 | // by the compiler. 22 | var BuildDate string 23 | 24 | // Version defines the main version number that is being run at the moment. This 25 | // will be filled by the compiler. 26 | var Version = "0.0.0-no-proper-build" 27 | --------------------------------------------------------------------------------